개발 공부/Java

이펙티브 자바 아이템 18 - 상속보다는 컴포지션을 사용하라 - 완벽 공략

개발인생 2022. 11. 24. 09:11
반응형

아이템 18 - 상속보다는 컴포지션을 사용하라 - 완벽 공략

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

데코레이터 패턴

데코레이터 패턴은 기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴 이다.

데코레이터는 상속이 아니라 위임 을 사용하는 대표적인 디자인 패턴 중 하나이다.

굳이 새로운 클래스를 만들 필요없이 기존의 클래스를 조합해 새로운 인스턴스를 만들 수 있다.

단점으로는 조합을 하는 코드가 복잡해 질 수 있다.

public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           {
       System.out.println("?????");
       return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   {
                                      System.out.println("?AWDASD");
                                      return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

ForwardingSet 클래스는 데코레이터 역활을 한다.

public class InstrumentedSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override public boolean add(E e) {
        addCount++;
        return super.add(e);
    }
    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
    public int getAddCount() {
        return addCount;
    }

    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
        s.addAll(List.of("틱", "탁탁", "펑"));
        System.out.println(s.getAddCount());
    }
}

InstrumentedSet 클래스는 Concrete 데코레이터 역할을 한다.

HashSet 을 감싸고 있으므로 HashSet 이 Concrete 컴포넌트가 된다.

콜백 프레임워크와 셀프 문제

콜백이란 어떤 함수에 다른 함수를 인자로 전달하는 걸 말한다.

interface FunctionToCall {

    void call();

    void run();
}
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() {
        this.service.run(this);
    }
}

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);
        bobFunction.run();
    }
}

Service 클래스에서 필요한 순간에 넘겨진 객체를 호출해서 사용한다.

이게 콜백이다.

비동기적으로 함수를 호출하는데 사용된다.

지금까지의 코드에서는 잘 동작하는 것처럼 보이지만 Wrapper 와 같이 사용하게되면 셀프 문제 가 발생한다.

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() {
        this.bobFunction.run();
    }
}

BobFunctionWrapper 클래스는 메서드들을 bobFunction 에 위임한다.

결과에서는 우리가 기대했던 결과가 나오지 않는다.

    @Override
    public void run() {
        this.bobFunction.run();
    }

BobFunctionWrapper 클래스에서 호출하는 run 은 bobFunction 의 run() 이다.

// BobFunction
    @Override
    public void run() {
        this.service.run(this);
    }

이때 넘겨주게되는 this 는 BobFunction 이다.

때문에 우리가 기대한 BobFunctionWrapper 의 call() 을 호출한게 아닌 BobFunction 클래스의 call() 을 호출하게된다.

이래서 셀프 문제 라고 한다.

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

반응형