18. 상속보다는 컴포지션을 사용하라

신명철·2022년 2월 16일
0

Effective Java

목록 보기
16/80

컴포지션?
private 필드를 통해 기존 클래스가 새로운 클래스의 구성요소(인스턴스)로 쓰이는 것
새로운 클래스에 기존 클래스의 영향이 적어 기존 클래스가 변경되어도 안전

예시

public class NoteBook{
	private final Keyboard keyboard;
    ...
    public Notebook(Keyboard keyboard){
    	this.keyboard = keyboard;
    }
}
public class Keyboard{
	...
}

상속의 문제점

  • 상속은 캡슐화를 깨뜨린다.
    • 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생기기 때문이다.
public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;
    public InstrumentedHashSet() {

    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @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 class main {
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("tic", "tac", "toe"));
        System.out.println(s.getAddCount());
    }
}
  • 위 코드는 보기에는 정상적으로 작동할 것처럼 보이지만 그렇지 않다.
    • 결과 값이 3이 아닌 6으로 출력된다.
      public boolean addAll(Collection<? extends E> c) {
      boolean modified = false;
      for (E e : c)
          if (add(e))
              modified = true;
      return modified;   
    • HashSet의 addAll은 InstrumentedHashSet에서 재정의된 add를 호출하기 때문에 addAll에서 +3, add에서 +3, 총 6이 된다

상속 대신 컴포지션

기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 한다.

  • 컴포지션을 통해 새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환한다. 이를 forwarding 이라 한다.
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> set;
    
    public ForwardingSet(Set<E> set) { this.set = set; }
    
    public void clear() { set.clear(); }
    public boolean isEmpty() { return set.isEmpbty(); }
    public int size() { return s.size(); }
    public boolean add(E e) { return set.add(e); }
    public boolean addAll(Collection<? extends E> c) { return set.addAll(c); }
    
    ...
}

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> collection) {
        addCount = addCount + collection.size();
        return super.addAll(collection);
    }

    public int getAddCount() {
        return addCount;
    }
}
  • 위와 같이 구현하면 InstrumentedSet은 HashSet의 모든 기능을 정의한 Set 인터페이스를 활용해 설계되어서 견고하고, 유연성이 높다
  • 다른 Set 인터페이스를 감싸고 있다는 듯에서 InstrumentedSet은 래퍼 클래스(Wrapper Class)라고 하고, 다른 Set 에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern)이라고 한다.

래퍼 클래스는 단점이 거의 없다. 단 콜백 프레임워크와는 어울리지 않는다는 점을 주의하자. 콜백 프레임워크는 자기 자신의 참조를 다른 객체에 넘겨서 다음 콜백 때 사용하도록 한다. 내부 객체는 자신을 래핑하고 있는 래퍼의 존재를 모르니 this의 참조를 넘기고 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다.

상속 사용 시 확인 사항

  • 확장하려는 클래스의 API 에 결함이 없는가?
  • 결함이 있다면, 이 결함이 새로운 클래스의 API 까지 전파되도 괜찮은가?

컴포지션은 이러한 결함을 숨기는 새로운 API를 설계할 수 있지만, 상속은 상위클래스의 API 결함까지도 그대로 상속한다.

profile
내 머릿속 지우개

0개의 댓글