저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

신림프로그래머 모임에 발표할 모델링 연습 리뷰 자료입니다.



발표 자료에 나오는 JPA의 AttributeConverter에 대한 내용은 아래 링크에 정리했습니다.

  • http://javacan.tistory.com/entry/How-to-use-JPA-21-by-AttributeConverter-for-custom-value-type


Posted by 최범균 madvirus

댓글을 달아 주세요


Posted by 최범균 madvirus

댓글을 달아 주세요

Adapter 패턴 요약한 것



Posted by 최범균 madvirus

댓글을 달아 주세요

객체 지향을 하는 이유는? 앞서 '객체 지향 기초 이야기 1, 객체(http://javacan.tistory.com/entry/OO-Basic-1-Object)' 글에서 유지보수 비용을 절감하기 위해 객체 지향적으로 개발한다고 했던 것을 기억할 것이다. 유지보수 비용을 줄일 수 있다는 것은 기능을 쉽게 추가하거나 변경할 수 있다는 것을 의미하는데, 이는 다음의 두 가지를 통해서 얻을 수 있다.
  • 캡슐화: 내부의 구현이 변경되더라도 외부에 영향을 최소화
  • 다형성: 구현체 변경의 유현함을 제공
이번에 살펴볼 내용은 위 두 가지 중 다형성이 주는 유연함에 대한 것이다.

유연하지 않은 코드


유연한 코드에 대해 살펴보기 전에 먼저 유연하지 않은 코드를 살펴보자. 앞서 예제들에서 사용했던 로그 수집을 생각해보자. 최초에 로그 처리는 FTP로부터 파일을 읽어오는 요구만 있었다. 그래서 로그 처리 과정을 제어하는 객체는 FTP로부터 로그를 다운받아오는 FtpLogCollector를 사용해서 구현하였다.


// 흐름 제어

FtpLogCollector logCollector = new FtpLogCollector(....);

FtpLogSet logSet = logCollector.collect();

....


그런데, 요구 사항이 갑자기 바뀌었다. FTP가 아닌 DB에 쌓여 있는 데이터를 로그로 사용해야 한다는 것이었다. 이제 해야 하는 일은? DB에서 로그를 읽어오는 객체를 구현하고 FtpLogCollector 클래스가 출현하는 부분을 모두 DbLogCollector로 바꿔주는 것이다.


DbLogCollector logCollector = new DbLogCollector(....);

DbLogSet logSet = logCollector.collect();

....


로그의 수집 방식이 바뀌면, 로구를 수집하는 구현 클래스가 바뀌는 것 뿐만 아니라 수집 기능을 사용하는 코드도 바뀌게 된다. 즉, 하나의 변화가 다른 코드의 변화를 유발시키는 것이다. 이런 걸 당해본 개발자들은 알겠지만, 이런 종류의 일은 어딜 변경해야 하는지 코드를 검색해야 하고, 노동 집약적인 방식으로 일을 하도록 만든다.


이런 일이 벌어지는 이유는 추상화된 타입이 아닌 실제 구현체를 이용해서 코드를 만들었기 때문이다.


구현체(implementation)가 아닌 인터페이스(interface)에 대고 프로그래밍하기


객체 지향의 기본 규칙 중에 '구현체가 아닌 인터페이스에 대고 프로그래밍'하라는 규칙이 있다. 이 규칙이 나온 이유는 바로 유연함 때문이다. 앞서 봤듯이 추상화된 인터페이스 타입이 아닌 실제 구현을 제공하는 타입을 이용해서 프로그래밍 할 경우, 구현에 변화가 생겼을 때 변화의 영향력이 이곳 저곳으로 퍼져 나가게 된다.


변화가 발생하는 부분이 있다면, 이 부분을 추상화해서 인터페이스 타입을 이용해서 프로그래밍 해 주어야 한다. 인터페이스 타입을 사용해서 구현하게 되면, 실제 구현 객체가 변경되더라도 인터페이스 타입을 이용해서 구현한 코드는 영향을 받지 않게 된다. 예를 들어, 아래는 FtpLogCollector 대신 로그 수집을 추상화한 LogCollector 인터페이스에 대고 프로그래밍을 한 코드이다.


LogCollector logCollector = ... // LogCollector 구현 객체를 어디선가 구함

LogSet logSet = logCollector.collect();

...


LogCollector의 실제 구현 객체를 구하는 부분은 일단 잊어두고 나머지 코드를 살펴보자. 로그 수집하는 방식이 FTP에서 DB로 바뀌더라도 위 코드는 바뀌지 않는다. DB 뿐만 아니라 어떤 방식으로 바뀌더라도, 심지어 FTP와 DB에서 모두 로그를 읽어오도록 구현하더라도 위 코드는 바뀌지 않는다. 즉, 인터페이스에 대고 프로그래밍함으로써 실제 사용되는 구현 객체를 쉽게 변경할 수 있는 유연함을 얻게 된 것이다.


위 코드에서 바뀌는 부분은 실제 LogCollector 구현 객체를 생성하는 부분이다. 실제 사용할 구현 객체를 구하는  부분은 Factory 패턴을 사용하거나 DI(Dependecy Injection)을 통해서 코드 변경을 최소화할 수 있는데, 이 두 가지에 대한 내용은 본 글의 범위를 벗어나므로 패턴 관련 서적 등을 참고하기 바란다.


하지만, 유연함을 얻는 과정은 간단하지 않다. 먼저 실제 구현 객체를 추상화한 인터페이스를 도출해내야 한다. 처음 객체 지향을 접하는 사람들이 어려워 하는 것 중의 하나가 객체를 알맞게 추상화한 인터페이스를 도출하는 것이기 때문에, 유연한 코드를 얻어 내려면 기능(객체)을 추상화하는 연습을 많이 해 주어야 한다.


인터페이스를 도출하기 위한 기본적인 방법은 구현 클래스의 public 메서드를 인터페이스로 변경해보는 것이다. public 메서드는 외부에 기능을 제공할 목적으로 만들어지는 경우가 많기 때문에 인터페이스 후보가 될 수 있다. 내부의 private 메서드 중에서도 인터페이스 후보가 나올 수 있다. private 메서드는 클래스 내부에서 사용되는 기능을 분리한 경우가 많은데, 이 경우 private 메서드를 별도의 인터페이스로 분리함으로써 해당 기능에 대한 유연함을 얻을 수 있게 된다.


인터페이스를 항상 만들어야 하나?


당장 발생하지도 않을 변화를 준비하느라 미리 앞서 인터페이스를 사용할 필요는 없다. 실제로 모든 구현 타입에 대해 인터페이스 타입을 만드는 것은 매우 성가시면서도 불필요한 일이 될 수 있다. 하지만, 그럼에도 불구하고 필자는 인터페이스를 최대한 도출해내려고 노력하는데, 그 이유 중 하나는 단위 테스트에 필요한 Mock 테스트를 할 수 있도록 하기 위함이다.


구현을 하다보면 다른 기능이 구현되지 않아서 현재 작업중인 코드를 테스트 할 수 없는 경우가 발생하는데, 이 경우 Mock 객체를 사용하면 다른 기능의 구현이 되어 있지 않더라도 작업중인 코드를 어느 정도 테스트 할 수 있게 된다. 다른 기능이 만약 인터페이스로 추상화되어 있다면 여러 Mock 라이브러리를 사용해서 다른 기능을 가짜로 제공할 수 있게 되며, 이는 다른 기능의 구현이 완성되어 있지 않더라도 현재 기능을 테스트 할 수 있다는 것을 의미한다.


관련글

참고자료


제가 쓴 객체 지향 입문서입니다.


http://www.aladin.co.kr/shop/wproduct.aspx?ISBN=8969090010  에서 확인하실 수 있습니다.


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 권남 2012.05.29 12:28 신고  댓글주소  수정/삭제  댓글쓰기

    잘 읽고 있습니다.
    참 쉽게 설명하시네요.

1월 28일 예정되어 있는 DDD 쌩기초 세미나 발표 자료입니다.
세미나 관련 정보는 http://cafe.daum.net/javacan/9Voo/88 에서 확인할 수 있습니다.
 
Posted by 최범균 madvirus

댓글을 달아 주세요

많은 입문서에서 캡슐화(encapsulation)를 단순히 private을 사용해서 필드를 외부에 감추는 것 정도로 소개하고 넘어가는 경우가 많아, 많은 입문자들에게 오해를 심어주는 것 같다. 그래서 집필 중인 자바 프로그래밍 입문서에서 캡슐화와 관련된 부분을 블로그를 통해 공개하고자 한다. 아래 내용부터는 (아직 집필중인) 책의 캡슐화 부분을 발체한 내용이다.

* 본 문서는 책의 일부 내용을 발췌한 것으로서 온오프라인 상의 무단 배포를 금합니다.

캡슐화(encapsulation)

지금까지 자바에서 클래스를 만드는 방법을 살펴봤다. 필드를 이용해서 객체의 상태를 보관하고, 메서드를 이용해서 기능을 구현하고, 생성자를 이용해서 객체를 생성하는 방법을 공부 했다. 또한, public, protected, private을 이용한 접근 제어 방법도 살펴봤다.

그럼, 왜 클래스를 이용하는 걸까? C 언어의 구조체와 함수를 이용해도 자바의 클래스와 비슷한 구현이 가능할 것 같은데 말이다. 클래스를 사용하는 이유는 객체 지향 방식으로 구현하기 위해서이다. 자바는 클래스를 이용해서 객체를 표현하고 있으며, 자바 언어로 객체 지향을 잘 하기 위해서는 객체 지향 원칙에 따라 클래스를 작성해 주어야 한다.

그렇다면, 객체 지향의 가장 기본이 되는 원칙은 무엇일까? 바로 이 절의 제목인 '캡슐화(encapsulation)'가 객체 지향의 가장 기본이 되는 대원칙이다. 본 절에서는 완벽하게는 아니어도 조금이나마 독자가 캡슐화가 무엇이고 왜 캡슐화가 중요한 지 느낄 수 있도록 할 것이다.

흔히 객체 지향과 반대되는 개념으로 절차 지향을 언급하는데, 본 절에서는 아주 간단한 스톱워치 기능을 구현하는 과정을 통해서 절차 지향의 단점을 파악하고 캡슐화를 통해 어떻게 이 문제를 해결할 수 있는 지 살펴 볼 것이다.

절차 지향 방식의 구현

성능 측정 어플리케이션을 개발해서 고객에게 납품하기로 했다고 하자. 성능 측정을 위해 필요한 공통 기능은 실행 시간을 측정하는 것이다. 그래서 시간 데이터를 표현하기 위해 다음과 같이 밀리초(1/1000초) 단위로 시간을 보관할 수 있는 클래스를 작성하였다.

package ch03.util;

public class ProceduralStopWatch {

    public long startTime; // 밀리초(1/1000초) 단위
    public long stopTime; // 1/1000초 단위

    public long getElapsedTime() {
        return stopTime - startTime;
    }
}

ProceduralStopWatch 클래스는 시작 시간과 종료 시간을 저장할 수 있는 두 개의 public 필드를 제공하고 있다. 성능 측정 모듈에서 종료 시간과 시작 시간 사이의 차이를 구하는 코드가 많기 때문에, 흘러간 시간을 쉽게 구할 수 있도록 getElapsedTime() 메서드를 추가로 구현하였다.

이제 ProceduralStopWatch 클래스를 사용하는 코드는 다음과 같은 방식으로 작성될 것이다.

ProceduralStopWatch stopWatch = new ProceduralStopWatch();
stopWatch.startTime = System.currentTimeMillis(); // 시작 시간 설정

// 측정 대상 기능 실행

stopWatch.stopTime = System.currentTimeMillis(); // 종료 시간 설정
long elapsedTime = stopWatch.getElapsedTime(); // 시간 차이 구함

수 개월을 문제 없이 개발해 나갔다. 그런데, 늘 그렇듯이 고객으로부터 요구 사항을 변경하자는 연락이 왔다. 필요에 따라 밀리초(1/1000초) 보다 더 세밀한 단위로 측정할 필요가 있기 때문에 밀리초뿐만 아니라 더 세밀한 단위로도 측정할 수 있게 해 달라는 것이었다.

아주 간단한 방법은 다음과 같이 나노초 단위로 측정이 필요한 코드에서만 startTime 필드와 stopTime 필드에 나노초 단위로 시간을 저장하는 것이다.

ProceduralStopWatch stopWatch = new ProceduralStopWatch();
stopWatch.startTime = System.nanoTime(); // 시작 시간 나노초 단위 설정

// 측정 대상 기능 실행

stopWatch.stopTime = System.nanoTime(); // 종료 시간 나노초 단위 설정
long elapsedTime = stopWatch.getElapsedTime(); // 시간 차이 나노초 단위로 구함

하지만, startTime 필드에 어떤 경우에는 밀리초 단위의 값을 보관하고 어떤 경우에는 나노초 단위의 값을 보관하게 되면, 개발 과정뿐만 아니라 유지보수 과정에서 문제가 발생할 것 같다. 이미 개발자 중 몇 명은 기준 없이 밀리초와 나노초를 섞어서 사용하기 시작했다.

그래서, 나노초 단위를 저장할 수 있도록 ProceduralStopWatch 클래스에 startNanoTime, stopNanoTime의 두 필드와 getElapsedNanoTime() 메서드를 추가하기로 결심했다.

package ch03.util;

public class ProceduralStopWatch {

    public long startTime;
    public long stopTime;
   
    public long startNanoTime;
    public long stopNanoTime;

    public long getElapsedTime() {
        return stopTime - startTime;
    }
   
    public long getElapsedNanoTime() {
        return stopNanoTime - startNanoTime;
    }
}

이제 나노초 단위를 이용해서 시간을 측정해야 하는 코드를 다음과 같이 변경함으로써, 시간 단위가 나노초 임을 분명히 할 수 있게 되었다.

ProceduralStopWatch stopWatch = new ProceduralStopWatch();
stopWatch.startNanoTime = System.nanoTime(); // 시작 시간 나노초 단위 설정

// 측정 대상 기능 실행

stopWatch.stopNanoTime = System.nanoTime(); // 종료 시간 나노초 단위 설정
long elapsedTime = stopWatch.getElapsedNanoTime(); // 시간 차이 나노초 단위로 구함

일단, 급한 불은 껐다. 그런데, 여전히 불안함이 떠나질 않는다. 만약 고객이 초 단위로 값을 구해 달라는 요구가 추가되면 어떻게 해야 하나? 또는 같은 시간을 초 단위, 밀리초 단위, 나노초 단위로 표현해 달라고 하면 어떻게 해야 하나? 아마 코드는 점점 복잡해지고 요구사항이 추가되거나 변경될 때마다 함께 수정되는 코드도 많아질 것이다.

자, 그러면 왜 이런 일이 벌어졌을까? 가장 큰 이유는 데이터를 중심으로 개발했기 때문이다. (데이터를 중심으로 개발하는 것이 전형적인 절차 지향 방식 개발이다.)

[그림3.18] 데이터를 중심으로 개발되는 절차 지향 방식


ProceduralStopWatch 객체를 사용하는 코드는 객체의 필드에 직접 값을 할당하고 값을 가져올 수 있다. 편의상 getElapsedTime() 메서드를 만들었지만, 마음대로 필드의 값을 조작할 수 있다. ProceduralStopWatch 객체의 내부인 startTime 필드와 stopTime 필드를 마음대로 접근할 수 있기 때문에 많은 코드들이 직접 ProceduralStopWatch 객체의 필드 데이터를 조작할 수 있으며, 이로 인해 ProceduralStopWatch의 내부 코드를 변경하게 되면 많은 코드의 변화를 유발하게 된다.

[그림3.19] 절차 지향에서는 데이터의 변화가 많은 코드의 변화를 유발시킴

요구 사항의 변화가 없다면 또는 추가되지 않는다면, 데이터를 중심으로 개발하는 게 문제가 되지 않는다. 하지만, 거의 모든 프로젝트에서 요구사항은 추가되거나 변경되기 마련이며, 데이터를 중심으로 개발할 경우 데이터의 변화는 많은 코드에 수정을 발생시키는 요인이 된다.

그렇다면 어떻게 해야 ProceduralStopWatch 클래스를 사용하는 코드에 영향을 주지 않으면서 (또는 영향을 최소화하면서) ProceduralStopWatch 클래스의 내부 데이터 구조를 변경할 수 있을까? 정답은 바로 ProceduralStopWatch 클래스의 기능을 캡슐화하는 것이다.

객체 지향 방식의 구현

스톱워치 예를 객체 지향 방식으로 재구성해 보겠다. 객체 지향의 핵심 중의 핵심은 캡슐화에 있다. 캡슐화는 자세한 내부 구현을 외부에 드러내지 않고 숨기는 것이다. 캡슐화를 하게 되면 내부에 데이터를 어떻게 저장하는 지, 그 데이터를 어떻게 처리하는 지, 또는 특정 기능을 어떻게 제공하는 지에 대한 내용은 드러내지 않는다. 단지, 객체가 어떤 기능을 제공하는 지만 공유하게 된다.

예를 들어, 스톱워치 예의 경우 시간 데이터를 어떻게 구하는 지 또는 어떤 타입으로 저장하는 지 등의 구현은 외부로 드러내지 않고 다음의 기능만 외부에 제공하게 된다.
  • 스톱워치를 시작한다
  • 스톱워치를 중지한다.
  •  중지와 시작 사이의 시간 차이를 구한다.
스톱워치 기능을 제공하는 클래스를 캡슐화해서 다시 구현해 보자. 아래 코드는 객체 지향 방식으로 새롭게 구현한 스톱워치 클래스이다.

package ch03.util;

public class StopWatch {

    private long startTime;
    private long stopTime;

    public void start() {
        startTime = System.currentTimeMillis();
    }

    public void stop() {
        stopTime = System.currentTimeMillis();
    }

    public Time getElapsedTime() {
        return new Time(stopTime - startTime);
    }
}

StopWatch 클래스는 시작 시간과 종료 시간을 보관하기 위해 startTime 필드와 stopTime 필드를 사용하는데, 이 두 필드는 private 이다. 따라서, StopWatch 클래스를 제외한 다른 코드에서는 이 두 필드에 직접 접근할 수 없다.

start() 메서드는 startTime 필드에 현재 시간 값을 밀리초 단위로 저장한다. stop() 메서드도 유사하게 stopTime 필드에 밀리초 단위로 현재 시간 값을 저장한다. 즉, 스톱워치의 시작과 중지를 처리하려면 이 두 메서드를 호출해야 한다.

getElapsedTime()은 long이 아닌 Time이라는 클래스 타입을 리턴한다. Time은 시간을 표현하기 위해 만든 클래스로서 아래와 같다.

package ch03.util;

public class Time {

    private long t;
   
    public Time(long t) {
        this.t = t;
    }

    public long getMilliTime() {
        return t;
    }
}

StopWatch 클래스를 사용해서 시간 차를 구하는 코드는 다음과 같을 것이다.

StopWatch stopWatch = new StopWatch();
stopWatch.start(); // startTime 필드에 값을 할당하는 게 아닌, 기능 실행
// 코드
stopWatch.stop(); // stopTime 필드에 값을 할당하는 게 아닌, 기능 실행
Time time = stopWatch.getElapsedTime(); // long 타입이 아닌 시간을 표현하는 타입
// time.getMilliTime() 사용

앞서 절차 지향 방식에서는 필드에 직접 접근해서 필드 값을 변경했던 것에 반해 객체 지향 방식에서는 상세한 구현이 start() 메서드와 stop() 메서드로 캡슐화 되어 있다. StopWatch 클래스를 사용하는 코드는 start() 메서드가 내부적으로 어떻게 시작 시간을 저장하는 지에 대해서는 알 필요 없이, start() 메서드가 스톱워치의 시작 기능을 제공한다는 것만 알면 된다.

스톱워치를 중지한 뒤 흘러간 시간을 구할 때 사용된 리턴 타입은 long이 아닌 Time 클래스이다. long이 아닌 Time 클래스를 사용한 이유는 long은 시간을 표현하는 데 적합하지 않기 때문이다. 그래서 시간을 표현하기 위해 Time 클래스를 추가로 만들었고, Time 클래스로부터 필요한 값을 구하도록 했다.

이제 고객으로부터 새로운 요구사항을 받을 차례가 됐다. 절차 지향 방식을 설명할 때와 동일하게 나노초 단위로도 시간 차이 값을 구할 수 있어야 된다고 한다. 절차 지향 방식에서 ProceduralStopWatch 클래스를 사용하는 수 많은 코드를 변경해야 했던 기억이 새록 새록 떠오를 것이다. 하지만, 걱정하지 마라. 우리는 이미 스톱워치와 시간을 캡슐화하는 데 성공했고, StopWatch 클래스와 Time 클래스를 사용하는 코드에 어떤 영향도 주지 않고 이 두 클래스를 변경할 수 있다.

먼저, StopWatch 클래스가 내부적으로 나노초를 저장하도록 변경해보자. 다음과 같이 밀리초를 가져오는 코드를 나노초를 가져오는 코드로 변경했다.

package ch03.util;

public class StopWatch {

    private long startTime;
    private long stopTime;

    public void start() {
        startTime = System.nanoTime();
    }

    public void stop() {
        stopTime = System.nanoTime();
    }

    public Time getElapsedTime() {
        return new Time(stopTime - startTime);
    }
}

Time 클래스가 생성자에서 입력받는 값이 밀리초에서 나노초로 변경되었으므로 Time 클래스를 다음과 같이 변경하였다. t 필드가 저장하는 값의 단위가 나노초이므로 getMilliTime() 메서드의 구현이 일부 변경되었고, getNanoTime()메서드가 새로 추가되었다.

package ch03.util;

public class Time {

    private long t;
   
    public Time(long t) {
        this.t = t;
    }

    public long getMilliTime() {
        return t / 1000000L;
    }

    public long getNanoTime() {
        return t;
    }
}

나노초 단위를 수용할 수 있도록 StopWatch 클래스와 Time 클래스를 변경하였다. 이제 이두 클래스를 사용하던 코드가 나노초를 사용하도록 변경할 차례이다. 바꿔보자.

StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 코드
stopWatch.stop();
Time time = stopWatch.getElapsedTime();
// time.getNanoTime() 사용

어떤가? 나노초 단위로 시간 차이를 구하기 위해서 변경한 코드는 Time 클래스의 getMilliTime() 메서드 대신 getNanoTime() 메서드를 사용하도록 변경한 것뿐이다. 나머지 코드는 하나도 변경되지 않았다.

고객으로부터 요구사항이 또 들어왔다. 초 단위로도 시간 차이를 표현하고 싶단다. 이건 아주 쉽다. 왜냐면 우리는 Time 클래스를 이용해서 시간을 표현하고 있기 때문이다. 다음과 같이 Time 클래스에 메서드 하나만 더 추가해주면 끝이다.

package ch03.util;

public class Time {
    ...
    public double getSecondTime() {
        return (double)t / (double)1000000000;
    }
}

자세한 내부 구현을 숨기고 외부에는 기능만을 제공하도록 StopWatch 클래스를 캡슐화 하였다. 그리고, 스톱워치의 요구 사항이 변경되었지만 StopWatch 클래스를 잘 캡슐화 한 덕분에 StopWatch 클래스를 사용하는 코드에 거의 영향을 주지 않고 StopWatch 클래스를 변경할 수 있었다.

이게 바로 캡슐화의 힘이다. 내부 구현(특히 데이터)을 외부에 노출하지 않고 기능을 잘 캡슐화하게 되면, 해당 기능을 변경해야 하는 상황이 발생할 경우 특정 클래스로만 변화가 수렴되는 특징을 갖게 된다. 변화가 여러 클래스로 확산되지 않기 때문에 그 만큼 캡슐화 된 클래스의 수정은 용이하게 된다.

[그림3.20] 캡슐화는 요구사항 변화에 따른 코드 수정 범위를 최소화 해 준다.

실제로 다수의 소프트웨어는 개발 과정뿐만 아니라 개발이 완료된 이후에도 요구사항의 추가, 버그 수정 등으로 코드를 수정하게 된다. 만약 절차 지향으로 개발했다면 이런 수정 과정에서 변경되는 코드의 범위는 점점 커지게 되고 시간이 흐를수록 변화의 폭은 더욱 증폭된다. 따라서 절차 지향 방식에서 무엇인가를 변경하는 것은 개발자 입장에서 많은 위험과 어려움이 따르는 작업이 된다.

반면에 객체 지향 방식으로 개발했다면 변화의 범위는 소수의 클래스로 한정되는 경향이 있으며, 따라서 새로운 요구 사항을 (절차 지향에 비해) 쉽고 빠르게 수용할 수 있게 된다. 우리는 이미 StopWatch 예제에서 이런 특징을 확인할 수 있었다.

Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 권남 2011.07.15 11:45 신고  댓글주소  수정/삭제  댓글쓰기

    멋진 설명 잘 읽었습니다. 책이 기대되네요.

  2. 2011.07.15 22:54  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다

  3. richpapa 2011.07.18 11:01 신고  댓글주소  수정/삭제  댓글쓰기

    기대됩니다. 언제 나오나요?

  4. 백명석 2011.08.04 16:41 신고  댓글주소  수정/삭제  댓글쓰기

    근데 Time 클래스에 요구사항이 늘어감에 따라 메소드가 추가되는 것이 보기 않 좋네.
    방법이 있지 않나 ?
    하나 또 오르는 방법이 있는데... 어찌 생각하나 ?

    • 최범균 madvirus 2011.08.05 08:50 신고  댓글주소  수정/삭제

      메서드가 증가가 많지 않을 것 같긴 하지만, 메서드가 계속 증가한다면 다음과 같은 메서드를 추가하면 어떨까 합니다.

      long elapsedTime(TimeUnit unit)

      이 경우에는 StopWatch 클래스에서 바로 위 클래스를 넣어도 될 듯 합니다. Time 클래스에 넣어야 한다면

      long value(TimeUnit unit) 정도의 이름이 좋을 것 같구요.

  5. 백명석 2011.08.05 09:14 신고  댓글주소  수정/삭제  댓글쓰기

    아니면 Visitor Pattern으로 원하는 timeUnit 별로 elapse Time을 계산하는 객체를 전달하여 계산하도록 하면 어떨까 ?
    long getElapsedTime(ElapsedTimeCalculateVisitor visitor) 이런 식으로.
    구조체는 새로운 타입 추가시 많은 변경을 유발하지만, 새로운 기능 추가시에는 해당 기능만 추가하면 되고,
    클래스는 polymorphism을 활용하여 변경 없이 새로운 타입 추가할 수 있지만, 기존 클래스에 새로운 기능 추가시에는 해당 클래스의 모든 클라이언트와 서브클래스까지 변경되어야 하는 재앙이 발생한다.
    그런데 이 같은 클래스의 문제는 Visitor로 해소할 수 있지 않을가 싶다 ^^

    • 최범균 madvirus 2011.08.05 09:40 신고  댓글주소  수정/삭제

      너무 많이 가시는 거 같아요. 저도 Visitor와 같은 double dispatch 방식을 생각해보긴 했는데, Visitor가 결국 StopWatch의 내장(시간 저장하기 위한 타입)을 알게 되는 상황이 발생해서 고민이 좀 되더라구요.

      그래서 Time을 좀 더 추상화해서 Time 객체가 크기 비교를 할 수 있는 기능을 추가하고, Time이 스스로 자기를 표현할 수 있도록 구현해주는 것도 생각해 볼 만 할 것 같아요.

      그런데요,,, 이 논의 거기 팀원들하고 마저 하심 안 되요? 제가 어제 술독에 빠졌다가 나와서 정신이 없어요.

  6. 백명석 2011.08.05 10:09 신고  댓글주소  수정/삭제  댓글쓰기

    ㅎㅎㅎ 이런 논의가 관심있는 많은 분들과 공유될 수 있었으면 좋겠다. 몸 추스려 ^^

  7. Kunny 2012.04.04 15:44 신고  댓글주소  수정/삭제  댓글쓰기

    디자인 패턴 책을 읽던중 패턴의 원칙중 하나는 변화하는것을 캡슐화한다고 적혀있었는데 도대체 뭐가 캡슐화인지 감이 안오던 중에 ..이 글을 보고 어렴풋이 이해하고 갑니다.

    사실 4일째 한번씩 보는중.

  8. 넘고 2012.08.21 15:52 신고  댓글주소  수정/삭제  댓글쓰기

    맨 처음 말씀하신대로 캡슐화에 대해 데이터를 숨기는 정도로만 알고 있었는데...
    정말 좋은글 감사합니다!

  9. 황윤성 2016.03.11 23:02 신고  댓글주소  수정/삭제  댓글쓰기

    읽고 나니까 제가 캡슐화에 대해서 잘 모르고 있었다고 깨달았어요

    공부 중인 학생인데 감사합니다!

  10. 맹구 2018.05.30 15:43 신고  댓글주소  수정/삭제  댓글쓰기

    과거에 캡슐화를 쓰지 않은 저를 용서할수 없게되었습니다...

    좋은 글 올려주셔서 감사합니다.

재사용을 위한 전통적인 객체지향 프로그래밍 접근법의 결점에 대한 극복

코드 재사용성 최대화-제 1 단계

코드의 재사용을 위한 방법에는 여러가지가 있다. 클래스의 상속은 그 중 한 방법이지만 연관성이 부족하므로 부분적으로 적합한 방법이라고 할 수 있다. 즉, 하나의 메소드를 재사용하기 위해서는 클래스 내의 다른 메소드와 멤버 데이터들도 상속해야 하기 때문이다. 불필요한 멤버들이 많아질수록 재사용을 위한 코드는 복잡해질 수 밖에 없다. 상속 받은 클래스의 부모 클래스에 대한 의존성은 또 다른 복잡성을 일으킨다. 부모 클래스에 가해진 변화는 자식 클래스들을 파괴한다. 부모 클래스와 자식 클래스 중 한쪽을 수정하면, 어떤 메소드가 오버라이드 되었는지 알기 어려워질 수 있으며, 오버라이드 메소드가 부모 클래스의 해당하는 메소드를 호출해야 하는지도 불분명해질 수 있다.

하나의 개념에 대한 작업을 수행하는 메소드는 자신의 부모 클래스에 의존할 수 있으므로 재사용할 수 있는 첫번째 대상이다. 이를 위하여, 고전적인 프로시져 프로그래밍처럼 코드를 클래스 인스턴스의 외부 메소드로 옮겨서 전역적으로 노출된 프로시져로 만든다. 그리고 이 프로시져의 재사용성을 높이기 위하여 코드를 정적 메소드로 만든다. 즉, 이 프로시져는 주어진 파라미터와 전역적으로 노출된 다른 프로시져만을 사용해야 하며, 로컬 변수 이외에는 사용하면 안된다. 외부 의존도를 축소시킴으로써 프로시져를 사용하는 복잡성을 감소시키고, 어디서나 재사용할 수 있는 가능성을 높인다. 물론, 코드를 재사용할 목적을 가지고 있지 않다고 해도, 이 구조는 확실히 더 명료하다.

자바에서는 클래스의 외부에 메소드가 존재할 수 없으므로, 관련된 프로시져들을 모아서 하나의 클래스 내에 전역적으로 노출된 정적 메소드로 만든다. 예를 들면, 다음과 같은 형태의 클래스를 생각해볼 수 있다.

class Polygon {
   public int getPerimeter() { ... }
   public int getArea() { ... }
}

이 코드를 다음과 같이 바꾼다.

class Polygon {
   public int getPerimeter() { return pPolygon.computePerimeter(this); }
   public int getArea() { return pPolygon.computeArea(this); }
}

그리고, pPolygon 클래스는 다음과 같다.

class pPolygon {
   static public int computePerimeter(Polygon polygon) { ... }
   static public int computeArea(Polygon polygon) { ... }
}

pPolygon 클래스는 Polygon 타입의 객체들과 관련된 프로시져들만을 반영하는 클래스이다. 이름 첫자의 p는 단지 이 클래스의 목적이 정적 프로시져들이 전역적으로 보이기 위하여 만든 그룹이라는 것을 나타낸다. 자바에서 클래스 이름이 소문자로 시작하는 것은 표준이 아니므로 pPolygon은 정상적인 클래스의 기능을 수행하기 위한 것이 아니라는 뜻도 포함된다. 즉, 객체를 만들기 위한 클래스가 아니라 단지 자바에서 사용되는 독립 요소일 뿐이다.

위의 예제에서 수행된 변화의 총체적인 효과는 클라이언트 코드가 기능의 재사용을 위하여 더 이상 Polygon 클래스를 상속하지 않아도 된다는 것이다. 그 기능은 이제 pPolygon 클래스에서 프로시져 원리로 사용될 수 있다. 클라이언트 코드는 필요한 기능성만을 사용하면서 필요 없는 기능들을 걱정할 필요가 없어진다.

이러한 새로운 프로시져 프로그래밍 스타일에서 클래스라는 단위가 유용한 목적으로 도움을 줄 수 없다는 것을 의미하는 것은 아니다. 반대로, 클래스는 그룹을 이루고 표현하는 객체의 멤버 필드들을 캡슐화하는 데 필요한 역할을 수행한다. 게다가, 복수의 인터페이스를 상속/구현함으로써 다형성을 지닐 수 있는 능력은 다음 단계에서 설명할 뛰어난 재사용성을 가능하게 해준다.

이 기법과 유사한 형태가 디자인 패턴 서적에 간단히 언급되어 있다. Strategy 패턴은 관련된 알고리즘들의 패밀리 멤버들을 공통 인터페이스 뒤에 캡슐화하여 클라이언트 코드가 그 알고리즘들을 상호교환할 수 있도록 만드는 것을 제안한다. 알고리즘은 보통 하나 또는 몇 개의 독립적인 프로시져로 코딩되므로, 이런 캡슐화는 여러 작업들을 수행하는 코드와 데이터를 포함하는 객체의 재사용을 넘어서 하나의 작업을 수행하는 프로시져의 재사용성을 강조한다.

그러나, 알고리즘을 인터페이스 뒤에 캡슐화하는 것은 알고리즘이 인터페이스를 구현한 객체로 코딩되어야 한다는 것을 암시한다. 이것은 프로시져가 여전히 내포된 객체의 데이터와 다른 메소드들과 커플링되어 있으며, 따라서, 재사용을 복잡하게 한다는 것을 의미한다. 또한 알고리즘이 필요할 때마다 그 객체를 생성해야 하며 프로그램의 성능을 저하시킨다. 디자인 패턴에서는 이 두가지 문제에 대한 해결책을 제시하고 있다. Strategy 패턴의 객체를 코딩할 때 Flyweight 패턴을 적용하면 잘 알려진 공유 인스턴스가 되며, 각 공유 객체는 액세스들 간에 상태를 유지하지 않는다. 즉, 객체는 멤버 데이터를 가지고 있지 않으므로 커플링 문제를 잘 처리할 수 있다. Flyweight-Strategy 패턴은 이 단계의 전역적으로 노출되고 상태가 없는 프로시져로 기능을 캡슐화한 기법과 매우 유사하다.

코드 재사용성 극대화-제 2 단계

클래스를 상속하는 것보다 인터페이스 파라미터 타입을 통하여 다형성의 장점을 추구하는 것이 객체지향 프로그래밍에서 재사용의 원리에 더 충실하다.

"클래스보다 인터페이스로 프로그래밍하는 것으로 재사용성을 얻을 수 있다. 메소드의 모든 파라미터가 이미 알고 있는 인터페이스에 대한 레퍼런스이고, 그 인터페이스가 전혀 알 수 없는 클래스에 의해 구현된다면, 그 메소드는 코드가 작성될 때 존재하지도 않는 클래스의 객체와도 작동할 수 있다. 기술적으로, 재사용 가능한 것은 메소드이며, 메소드로 전달되는 객체가 아니다."
이러한 사항을 1단계의 결과에 적용하면, 어떤 기능의 코드가 독립하여 전역적으로 노출된 프로시져이면, 클래스 타입의 입력 파라미터를 인터페이스 타입으로 바꿈으로서 재사용 가능성을 더 높일 수 있다. 그러면 인터페이스 타입을 구현한 어떤 클래스의 객체도 파라미터로 사용될 수 있게 된다. 따라서, 이 프로시져는 잠재적으로 다수의 객체 타입에 대하여 사용이 가능해진다. 예를 들면, 다음과 같은 전역적으로 노출된 정적 메소드를 생각해보자.

static public boolean contains(Rectangle rect, int x, int y) { ... } 

이 메소드는 주어진 사각형이 주어진 좌표를 포함하고 있는지 아닌지를 알려준다. 여기서 rect 파라미터를 클래스 타입 Rectangle에서 인터페이스 타입으로 바꾸어보자.

static public boolean contains(Rectangular rect, int x, int y) { ... }

Rectangular 인터페이스는 다음과 같다.

public interface Rectangular {
   Rectangle getBounds();
}

이제 Rectangular 인터페이스를 구현한 클래스의 객체가 pRectangular.contains() 메소드의 rect 파라미터로 전달될 것이다. 이 메소드는 전달되는 파라미터의 제약으로부터 느슨해지면서 재사용 가능성이 높아졌다.

그러나, 위의 예제에서 Rectangular 인터페이스의 getBounds() 메소드가 Rectangle을 반환한다면 실질적인 이점이 있는지 의아하게 생각될 것이다. 즉, Rectangle 객체가 전달될 것을 안다면 Rectangle 타입을 전달하는 대신에 Rectangular 인터페이스 타입을 전달하는 이유가 무엇인가하는 것이다. 이렇게 하는 가장 중요한 이유는 콜렉션을 다룰 때이다. 다음 메소드를 생각해보자.

static public boolean areAnyOverlapping(Collection rects) { ... }

위의 메소드는 주어진 콜렉션 내의 사각형 객체 중에 어느것이라도 중첩된 것이 있는지 알려주는 메소드이다. 이 메소드 내에서 콜렉션 내의 객체들을 차례로 순환할 때, 객체의 타입을 Rectangular 같은 인터페이스 타입으로 캐스트 할 수 없다면 객체의 사각좌표를 어떻게 구할 것인가. 유일한 방법은 객체를 원래의 지정된 클래스 타입으로 캐스트하는 것으로, 작동할 때 사용될 클래스 타입을 미리 메소드가 알아야하므로 그 타입의 재사용성이 제한된다. 이 단계에서 꼭 피해야만 할 점이다.

코드 재사용성 극대화-제 3 단계

2단계를 수행할 때, 주어진 클래스 타입을 대신하도록 어떤 인터페이스 타입을 선택해야 할까. 그 답은 프로시져가 파라미터로 필요로 하는 모든 것을 충분히 표현하면서도 부가적인 것들을 최소한으로 가진 것이다. 파라미터 객체가 구현할 것이 작은 인터페이스일수록, 어떤 특정한 클래스가 그 인터페이스를 구현하기에 좋고, 따라서 더 많은 수의 클래스의 객체가 파라미터가 될 수 있다. 다음과 같은 메소드를 보자.

static public boolean areOverlapping(Window window1, Window2) { ... }

두개의 (사각형인) 윈도우가 중첩되었는지 판별하는 메소드로, 이 메소드는 오직 두 파라미터의 사각좌표만을 필요로 하며, 따라서 이러한 사실을 반영하여 파라미터의 타입을 줄이는 것이 좋다.

static public boolean areOverlapping(Rectangular rect1, Rectangular rect2) { ... }

위의 코드는 앞의 Window 타입의 객체가 Rectangular 인터페이스를 구현할 수 있다는 것을 가정한다. 이제 모든 사각형 객체들에 대하여 첫번째 메소드에 포함된 기능을 재사용할 수 있다.

파라미터로부터 필요한 것들이 충분히 지정된 유용한 인터페이스가 불필요한 메소드들을 너무 많이 가지고 있는 경우를 경험한적이 있을 것이다. 이 경우에는 이런 모순에 직면한 다른 메소드에 의해 재사용될 수 있도록 전역 이름공간에 새로운 인터페이스를 정의하면 된다.

하나의 프로시져에 대하여 단 하나의 파라미터로부터 필요한 것을 지정하기 위한 단일 인터페이스를 만드는 것이 최적인 경우도 경험한 적이 있을 것이다. 그 인터페이스를 오직 그 파라미터에 대해서만 사용할 것이다. 보통 이런 경우는 C 언어에서 함수 포인터처럼 파라미터를 다루고 싶은 경우이다. 예를 들면, 다음과 같은 프로시져가 있다.

static public void sort(List list, SortComparison comp) { ... } 

이것은 주어진 리스트의 모든 객체를 제공된 비교 객체인 comp를 사용하여 비교하여 소트하는 것으로, 모든 sort가 comp로부터 원하는 것은 비교를 위해서 comp의 하나의 메소드를 호출하는 것이다. SortComparison은 단 하나의 메소드를 가진 인터페이스가 될 것이다.

public interface SortComparison {
   boolean comesBefore(Object a, Object b);
}

이 인터페이스의 단 하나의 목적은 sort 메소드가 그 역할을 할 수 있도록 기능을 제공하는 것이므로, SortComparison 인터페이스는 다른 곳에서는 재사용될 수 없다.

결론

이 세 단계는 전통적인 객체지향 기법을 사용하여 작성된 기존의 코드에 적용될 수 있으며, 또한, 객체지향 프로그래밍과 결합된 세 단계는 새로운 코드를 작성할 때 적용할 수 있는 새로운 기법들과 함께 사용될 수 있으며, 메소드의 재사용성과 결합능력을 증가시키면서 커플링과 복잡성을 감소시킨다.

물론, 이러한 단계를 원래부터 재사용에 부적절한 코드에 적용할 수는 없다. 그러한 코드는 보통 프로그램의 프레젠테이션 계층에서 주로 발견된다. 프로그램의 유저 인터페이스를 만드는 코드와 입력 이벤트를 실제 작업이 수행되는 프로시져에 연결하는 콘트롤 코드는 둘 다 프로그램마다 너무나 다른 기능을 보여주기 때문에 재사용일 불가능한 예이다.

본 글의 저작권은 작성자(이동훈)에게 있으며, 작성자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.
Posted by 최범균 madvirus

댓글을 달아 주세요