Effective Java : 아이템18. 완벽공략

    완벽 공략

    • p119, 데코레이터 패턴
    • p119, 컴포지션과 전달 조합은 넓은 의미로 위임(delegation)이라고 부른다.
    • p119, 콜백 프레임워크와 셀프 문제 

    데코레이터 패턴

    데코레이터 패턴은 다음 역할을 하는 패턴이다. 

    • 기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴.
    • 상속이 아닌 위임(컴포지션)을 사용해서 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하다. 

    일반적인 데코레이터 패턴의 모습은 위의 그림과 같다. 그렇지만 디자인 패턴은 '구조'가 중요한 것이 아니라 '의도'가 중요한 것이기 때문에 위의 형태를 가지는 것이 전형적인 데코레이터 패턴이구나, 라는 느낌만 가져가면 된다. 

     

    데코레이터 패턴의 장점

    데코레이터 패턴의 가장 큰 장점은 새로운 클래스를 기존 클래스를 Composition 해서 만들 수 있다. 새로운 클래스를 밑바닥부터 구현하지 않고, 이미 존재하는 클래스를 가져다 쓰면 된다는 것이다. 

     

    예를 들어 빨/주/파 지붕 + 대문을 각각 가진 '집'이라는 클래스를 만들어야 한다고 가정해보자. 만약 데코레이터 패턴을 사용하지 않는다면 처음부터 '집' 클래스를 하나씩 다 구현해야한다. 그리고 구현해야 하는 총 클래스는 3 * 3 = 9개의 클래스를 처음부터 모두 구현해야 한다. 

    • 빨빨 / 빨주 / 빨파 / 주빨 ... 등의 집 클래스 → 9개

     

    반면 Composition을 이용해서 이미 존재하는 클래스들을 조합해서 만든다면, 훨씬 편하게 사용할 수 있다. 개발자는 아래와 같이 구현하면 된다.

    • 지붕 : 빨 / 주 / 파 → 각각 1개씩 총 3개
    • 대문 : 빨 / 주 / 파 → 각각 1개씩 총 3개
    • 총 6개 구현

    즉, 데코레이터 패턴을 이용해서 Composition으로 구현한다면 구현해야 하는 코드의 양이 더욱 적어지는 효과가 있다. 

     

    이전 예제에 적용해본다면? 

    이전의 Set / HashSet / InstrumentedSet / ForwadingSet은 데코레이터 패턴의 전형적인 역할을 한다고 볼 수 있다. 각각의 클래스는 다음 역할에 대응된다. 

    • Set → Component
    • HashSet → ConcreteComponent
    • Decorator →  ForwardedSet
    • ConcreteDecorator → InstrumnetedSet

     

     


    완벽 공략 35. 콜백 프레임워크와 SELF 문제

    콜백 프레임워크와 래퍼를 같이 사용했을 때 발생할 수 있는 문제

    • 콜백 함수 : 다른 함수(A)의 인자로 전달된 함수(B)로, 해당 함수 (A) 내부에서 필요한 시점에 호출 될 수 있는 함수 (B)를 말한다.
    • Wrapper로 감싸고 있는 내부 객체가 어떤 클래스 (A)의 콜백으로 (B) 사용되는 경우일 때 콜백으로 this를 전달한다면, 해당 클래스 (A)는 callback으로 래퍼가 아닌 내부 객체를 호출한다. 이것을 SELF 문제라고 한다. 

    콜백함수는 어떤 함수에 인자로 전달된 함수다. 전달된 콜백 함수는 필요한 시점에 호출된다. 아래에서 코드로 Callback 함수를 살펴보자. 

    • BobFunction은 FunctionToCall 인터페이스를 구현한다. FunctionToCall은 콜백함수로 사용되기 위한 클래스다. 
    • Service는 콜백함수를 호출할 클래스다. 
      • BobFunction은 run() 메서드가 호출되면, 자기 일을 하다가 service.run()을 호출하면서 자기 자신(this)를 넘긴다.
      • service는 run()이 호출되면, 자기 자신이 할 일을 하다가. functionToCall을 이용해서 콜백 함수 call을 호출한다.

    위와 같이 동작한다. 여기서 한 가지 유의할 점은 파이썬 같은 경우에는 함수를 인자로 넘길 수 있지만, 자바이기 때문에 콜백 함수를 포함한 인스턴스를 넘겨준다. 

    public class BobFunction implements FunctionToCall{
    
        private final Service service;
    
        public BobFunction(Service service) {
            this.service = service;
        }
    
        @Override
        public void call() {
            System.out.println("밥을 먹을까..");
        }
    
        @Override
        public void run() {
            // 자기 일 끝나고, callback 함수를 준다.
            this.service.run(this);
        }
    }
    
    public class Service {
    
        public void run(FunctionToCall functionToCall) {
            System.out.println("뭐 좀 하다가...");
            // 콜백함수 호출
            functionToCall.call();
        }
    
        public static void main(String[] args) {
            Service service = new Service();
            BobFunction bobFunction = new BobFunction(service);
            bobFunction.run();
        }
    }

    위 코드의 main()을 실행시켜주면 다음 문자가 출력된다.

    뭐 좀 하다가...
    밥을 먹을까..

     

    SELF 문제의 발생

    위와 같이 콜백 함수를 사용하고 있을 때, 콜백함수를 감싼 Wrapper로 코드를 시작한다고 가정해보자. 그리고 Wrapper도 FunctionToCall 인터페이스를 구현했기 때문에 콜백함수 자체로 간주하고, 콜백함수로 Wrapper 클래스가 호출되면 좋겠다고 가정해보자. 

    목적은 다음과 같이 움직이는 것이다

    • BobFunctionWrapper.run() → BobFunction.run() → Service.run() → BobFunctionWrapper.call();
    public class BobFunctionWrapper implements FunctionToCall{
    
        private final BobFunction bobFunction;
    
        public BobFunctionWrapper(BobFunction bobFunction) {
            this.bobFunction = bobFunction;
        }
    
        @Override
        public void call() {
            this.bobFunction.call();
            System.out.println("커피도 마실까...");
        }
    
        @Override
        public void run() {
            bobFunction.run();
        }
    }
    
    public class Service {
    
        public void run(FunctionToCall functionToCall) {
            System.out.println("뭐 좀 하다가...");
            // 콜백함수 호출
            functionToCall.call();
        }
    
        public static void main(String[] args) {
            Service service = new Service();
            BobFunction bobFunction = new BobFunction(service);
            BobFunctionWrapper bobFunctionWrapper = new BobFunctionWrapper(bobFunction);
            bobFunctionWrapper.run();
    
        }
    }

    그렇지만 코드를 실행시켜보면 위에서 기대한 것처럼 동작하지 않는 것을 바로 알 수 있다. 왜 아래와 같은 문제가 발생하는 것일까?

    뭐 좀 하다가...
    밥을 먹을까..
    // 커피를 먹을까... --> 출력되지 않음.

    문제가 발생하는 이유는 전달된 callBack 함수가 BobFunction 인스턴스이기 때문이다. 아래 형식으로 코드가 흘러가는데, bobFunction에서 service.run()을 호출할 때 전달된 this는 bobFunction 인스턴스 내부에서 동작하기 때문에 BobFunction이 된다. 따라서 BobFunctionWrapper의 콜백이 호출되길 기대했지만, 실제로는 bobFunction의 callBack이 호출되는 것이다.

    BobFunctionWrapper.run() → bobFunction.run() ;
    bobFunction.run() → service.run(this);

    이런 문제가 Wrapper를 콜백 프레임워크와 함께 사용했을 때 발생할 수 있는 셀프 문제다.

    정리

    Wrapper로 콜백 함수를 감싸는 경우, Wrapper가 아닌 내부 객체가 호출된다. 따라서 이 부분을 잘 이해하고 콜백을 사용해야 한다. 

     

    댓글

    Designed by JB FACTORY