본문 바로가기
Effective Java

[Effective Java] 아이템18 상속보다는 컴포지션을 사용하라

by byeongoo 2021. 2. 21.

 상속의 위험성

상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전하다. 하지만 일반적인 구체 클래스를 패키지 경계를 넘어 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.

  • 상속은 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 
  • 상위 클래스의 릴리스마다 내부 구현이 달라질 수 있으므로 하위 클래스가 오동작할 수 있다. 
  • 자신의 다른 부분을 사용하는 '자기 사용'여부는 해당 클래스의 내부 구현에 해당 되며 다음 릴리스에서도 유지될 수 알 수 없다.
  • 하위 클래스에 추가한 새 메서드가 상위 클래스 다음 릴리즈에서 같은 시그니처를 가질 경우 컴파일도 되지 않는다.

 

컴포지션 설계

상속의 문제점을 피하기 위한 방법으로 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 방법이 있다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(구성)이라 한다.

  • 컴포지션(composition) : 기존 클래스가 새로운 클래스의 구성 요소로 쓰인다.  (private)
  • 전달(forwarding) : 새 클래스의 인스턴스를 참조하는 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다. 
  • 전달 메서드(forwarding method) : 새 클래스의 메서드

래퍼 클래스 - 상속 대신 컴포지션 사용

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 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) { 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) { 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(); }

}

 

■ 상속 원칙

상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인상황에서만 사용해야한다. 즉, 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야한다. 대답이 아닐 경우 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야하는 상황이 대다수다.