ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 18. 상속보다는 컴포지션을 사용하라
    Book/Effective Java 3E 2022. 10. 22. 09:40
    반응형

      상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아닙니다. 이 책에서의 '상속'은 클래스가 다른 클래스를 확장하는 구현 상속을 말합니다. 메서드 호출과는 달리 상속은 캡슐화를 깨뜨립니다. 다르게 말하면, 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있습니다. 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있다는 말입니다.

     

      상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법입니다. 확장할 목적으로 설계되었고 문서화도 잘 된 클래스(Item 19)도 마찬가지로 안전합니다. 하지만 일반적인 구체 클래스를 패키지 경계를 넘어, 즉 다른 패키지의 구체 클래스를 상속하는 일은 위험합니다.

     

      기존 클래스를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 합니다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(composition; 구성)이라 합니다. 새 클래스의 인스턴스 메서드들은 (private 필드를 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환합니다. 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 부릅니다.

    package effectivejava.chapter4.item18;
    import java.util.*;
    
    // 코드 18-2 래퍼 클래스 - 상속 대신 컴포지션을 사용했다.
    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());
        }
    }
    package effectivejava.chapter4.item18;
    import java.util.*;
    
    // 코드 18-3 재사용할 수 있는 전달 클래스
    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(); }
    }

      InstrumentedSet은 HashSet의 모든 기능을 정의한 Set 인터페이스를 활용해 설계되어 견고하고 아주 유연합니다. 다른 Set 인스턴스를 감싸고(wrap) 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern, [Gamma95])이라고 합니다. 컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고 부릅니다. 단, 엄밀히 따지면 래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우만 위임에 해당합니다.[Lieberman86; Gamma95]

     

      래퍼 클래스는 단점이 거의 없습니다. 한 가지, 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점만 주의하면 됩니다. 콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 합니다. 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 됩니다. 이를 SELF 문제라고 합니다. 잘 이해되지 않는다면 http://bit.ly/2LepViV에서 스택 오버플로의 답변을 참고하기 바랍니다.

     

    상속은 강력하지만 캡슐화를 해친다는 문제가 있다.

    상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 써야 한다.

    is-a 관계일 때도 안심할 수만은 없는 게, 하위 클래스의 패키지가 상위 클래스와 다르고,

    상위 클래스가 확장을 고려해 설계되지 않았다면 여전히 문제가 될 수 있다.

    상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자.

    특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다.

    래퍼 클래스는 하위 클래스보다 견고하고 강력하다.

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.