Algorithm Study

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

hyun-1200 2022. 6. 24. 14:58

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

  • 상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이지만, 다른 패키지의 구체클래스 를 상속하는 일은 위험하다.
  • 이 책에서 '상속'은 클래스가 다르 클래스를 확장하는 구현상속을 말한다.

상속이 위험한 이유

  • 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
    • 상위클래스가 어떻게 구현되느냐에 따라 하위클래스의 동작에 이상이 생길 수 있다.
  • 다음 릴리스에서 상위클래스에 새로운 메서드를 추가한다면?
    • ex. 컬렉션에 추가된 모든 원소가 특정조건을 만족해야만 하는 프로그램
    • 그 컬렉션을 상속하여 원소를 추가하는 모든 메서드를 재정의해 필요한 조건을 검사한다. 하지만, 상위클래스에 또 다른 원소 추가 메서득 만들어지면 문제가 발생
  • 하위클래스에 새로운 메서드르 추가하는 방법은 훨씬 안전하긴 하지만, 위험이 젆 없느 것은 아니다.
    • ex. 만약 상위클래스에 새 메서드가 추가됐는데, 운 없게도 하필 하위클래스에 추가한 메서드와 시그니처가 같고 반환타입으 다르며 클래스는 컴파일조차 되지않는다..

컴포지션(composition : 구성)

  • 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기조 클래스의 인스턴스를 참조하게 하느 방법 사용.
  • 기존 클래스가 새로우 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라 한다.
  • 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나며, 기존 클래스에 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.
// 래퍼클래스 - 상속 대신 컴포지션을 사용했다. 
public class InstrumentedSet<E> extends ForWardingSet<E> {
  private int addCount=0;
  
  public InstrumentedSet(Set<E> s){ supers(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); } 
  ...
  ... 
}
  • InstrumentedSet은 HashSet의 모든기능을 정의한 Set인터페이스를 활용해 설계되어 견고학 유연하다. 구체적으로는 Set 인터페이스를 구현했고, Set의 인스턴스를 인수로 받는 생성자를 하나 제공한다. 임의의 Set에 계측 기능을 덧씌워 새로운 Set을 만드는 것이 이 클래스의 핵심이다.
  • 다른 Set인스턴스를 감싸고(wrap)있다는 뜻에서 InstrumentedSet 같은 클래스르 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.
  • 상속은 반드시 하위 클래스가 상위 클래스으 '진짜' 하위 타입인 상황에서만 쓰여야 한다. ( = A is-a B ) -클래스 A를 상속하는 클래스 B를 작성하려 한다면 "B가 정말 A인가?" 자문해보고 "그렇다"고 확신할 수 없다면 B는 A를 상속해서는 안된다.
  • 컴포지션 대신 상속을 사용하길 결정하기 전에 "확장하려는 클래스의 API에 아무런 결함이 없는가? 결함이 있다면, 이 결함이 여러분 클래스의 API에 전파됃 괜찮은가?" 를 꼭 자문하라.

정리

상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다. is-a 관계일 때도 안심할 수만은 없는게, 하위클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제 될 수 있다. 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다. 래퍼클래스는 하위 클래스보다 견고하고 강력하다.