ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 18: Favor composition over inheritance
    독서/Effective Java 2021. 12. 19. 16:16

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

     

    상속은 코드 재사용을 달성하는 강력한 방법이지만 항상 최선은 아니다.부적절하게 사용하면 취약한 소프트웨어로 이어진다.

    메서드 호출과 달리 상속은 캡슐화를 위반한다. 즉, 하위 클래스는 적절한 기능을 위해 상위 클래스의 구현 세부 정보에 의존한다. 상위 클래스의 구현은 릴리스마다 변경될 수 있으며 변경될 경우 코드가 건드리지 않았더라도 하위 클래스에 영향이 가고, 결과적으로 하위 클래스는 상위 클래스의 작성자가 확장할 목적으로 특별히 설계하고 문서화하지 않는 한 상위 클래스와 함께 진화해야 한다.

    package com.tistory.povia.effectivejava.item18;
    
    import java.util.Collection;
    import java.util.HashSet;
    
    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;
      }
    }
    
    // 사용
    @Test
    void test(){
      InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
      s.addAll(Arrays.asList("Snap", "Crackle", "Pop"));
      log.info("s.size:{}", s.getAddCount());
    }
    
    // 결과 로그
    [Test worker] INFO com.tistory.povia.effectivejava.item18.InstrumentedHashSetTest - s.size:6

    사용 부분에서 s의 getAddCount 메서드가 3을 반환할 것으로 예상하지만 실제로는 6을 반환한다. 원인은, HashSet의 addAll 메서드에 있다.

    public boolean addAll(Collection<? extends E> c) {
      boolean modified = false;
      Iterator var3 = c.iterator();
    
      while(var3.hasNext()) {
        E e = var3.next();
        if (this.add(e)) {
          modified = true;
        }
      }
    
      return modified;
    }
    

     InstrumentedHashSet의 addAll 메소드는 addCount에 세 개를 추가한 다음 super.addAll을 사용하여 HashSet의 addAll 구현을 호출했다. 이것은 차례로 각 요소에 대해 한 번씩 InstrumentedHashSet에서 재정의된 add 메서드를 호출했고, 이 세 번의 호출 각각이 addCount를 증가해 결국 addCount는 6으로 변경됐다. 

     

    상속의 대안: 컴포지션

    기존 클래스를 확장하는 대신 기존 클래스의 인스턴스를 참조하는 private 필드를 클래스에 제공하는 방법을 사용하자. 이 디자인은 기존 클래스가 새 클래스의 구성 요소가 되기 때문에 컴포지션이라고 한다.

    새 클래스의 각 인스턴스 메서드는 기존 클래스의 포함된 인스턴스에서 해당 메서드를 호출하고 결과를 반환한다. 이를 전달(forwarding)이라고 하며 새 클래스의 메서드를 포워딩 메서드라고 한다.

    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();}
    }
    
    
    // 사용
    Set<Instant> times = new InstrumentedSet<>(new TreeSet<>(cmp));
    Set<E> s = new InstrumentedSet<>(new HashSet<>(INIT_CAPACITY));
    
    static void walk(Set<Dog> dogs) {
    	InstrumentedSet<Dog> iDogs = new InstrumentedSet<>(dogs);
    	// ... // Within this method use iDogs instead of dogs
    }

    InstrumentedSet 클래스는 각 InstrumentedSet 인스턴스가 다른 Set 인스턴스를 포함("래핑")하기 때문에 래퍼 클래스라 한다. 또한, InstrumentedSet 클래스가 계측을 추가하여 세트를 "장식"하기 때문에 데코레이터 패턴으로도 불린다. 때때로 구성과 전달의 조합을 위임이라고 하지만, 기술적으로 래퍼 객체가 내부 객체에 자신의 참조를 넘기는 경우만 해당한다. 
    래퍼 클래스 사용 시 주의할 점은 래퍼 클래스가 콜백 프레임워크에서 사용하기에 적합하지 않다는 것이다.

    https://stackoverflow.com/questions/28254116/wrapper-classes-are-not-suited-for-callback-frameworks

     

    상속을 써야하는 경우

    상속은 하위 클래스가 실제로 상위 클래스의 하위 타입인 경우에만 적합하다. 클래스 B는 두 클래스 사이에 "is-a" 관계가 존재하는 경우에만 클래스 A를 확장해야 한다. 모든 B가 정말 A일까? 에 대해 "yes"라 할 수 없다면 B는 A를 확장하면 안된다. 

     

    정리

    상속은 강력하지만 캡슐화를 위반하기 때문에 문제가 된다. 하위 클래스와 상위 클래스 사이에 진정한 하위 유형 관계가 있는 경우에만 적합하다. 그럼에도 불구하고 하위 클래스가 상위 클래스와 다른 패키지에 있고 상위 클래스가 상속을 위해 설계되지 않은 경우 상속으로 인해 취약성이 발생할 수 있으니 상속 대신 컴포지션을 사용하자. 특히 래퍼 클래스를 구현하기 위한 적절한 인터페이스가 있는 경우에는 더욱 사용하자.

    댓글

Designed by Tistory.