ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] 컴포지션을 활용한 코드의 재사용 방법 (Effective Java 3rd_Item18)
    Java 2019. 8. 4. 01:38
    반응형

     상속은 상위 클래스의 코드를 재사용할 수 있도록 해주지만 캡슐화를 깨뜨립니다. 즉, 상위 클래스가 어떻게 구현되느냐에 따라서 하위 클래스의 동작에 영향을 미칠 수 있습니다. 그렇기 때문에 상위 클래스의 설계자는 확장을 충분히 고려하고, 문서화를 해두어야 합니다.

     

     Effective Java 3rd 책의 예제 코드를 한 번 살펴보겠습니다.

    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 this.addCount;
        }
    }
        @Test
        public void 잘못된_예제_테스트() {
            InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
            s.addAll(Arrays.asList("하나", "둘", "셋"));
    
            System.out.println("this count is : " + s.getAddCount());
        }

     위의 코드는 HashSet 을 확장하여 처음 생성된 이후로 원소가 몇 개가 추가되었는지 카운트하는 클래스입니다. 만약 위의 클래스에서 테스트 코드와 같이 String 타입의 원소를 3개 추가했다면 addCount는 몇이 될까요?? 저는 간단하게 1차원적으로 생각하여 3이 될거라 생각했습니다.

    this count is : 6

     하지만 결과는 6을 출력하였습니다. 처음에는 뭐지? 싶었습니다. 그 이유에 대한 내용을 살펴보니 addAll() 메서드는 내부적으로 add() 메서드를 이용한다는 것이었습니다. 즉, 처음 addCount += c.size() 로 인하여 3이 더해지고, 상위 클래스의 addAll() 메서드가 내부적으로 add() 메서드를 수행하여 addCount++ 가 3번 호출되어 총 6이 된 것입니다.

        /**
         * {@inheritDoc}
         *
         * <p>This implementation iterates over the specified collection, and adds
         * each object returned by the iterator to this collection, in turn.
         *
         * <p>Note that this implementation will throw an
         * <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is
         * overridden (assuming the specified collection is non-empty).
         *
         * @throws UnsupportedOperationException {@inheritDoc}
         * @throws ClassCastException            {@inheritDoc}
         * @throws NullPointerException          {@inheritDoc}
         * @throws IllegalArgumentException      {@inheritDoc}
         * @throws IllegalStateException         {@inheritDoc}
         *
         * @see #add(Object)
         */
        public boolean addAll(Collection<? extends E> c) {
            boolean modified = false;
            for (E e : c)
                if (add(e))
                    modified = true;
            return modified;
        }

      HastSet의 추상하 클래스인 AbstractCollection 클래스에서 동작하는 addAll() 클래스입니다. addAll()을 보시면 반복문을 통해 add() 메서드가 호출되는 것을 확인할 수 있습니다.

     

     


     

     이러한 문제를 피해가기 위해서 기존 클래스를 상속하는 대신, 컴포지션을 이용하여 기존 클래스의 인스턴스를 private 필드로 참조하게 하는 방법을 사용할 수 있습니다. 새로운 클래스의 인스턴스는 기존 클래스의 대응하는 메서드를 호출하여 그 결과를 반환하는 방식을 전달(forwarding) 이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method) 라고 합니다. 이러한 방법은 새로운 클래스가 기존 클래스의 내부 구현 방식의 영향에서 벗어날 수 있도록 해줍니다.

     

     위에서 보았던 InstrumentedSet 클래스에서 컴포지션과 전달(forwarding) 방식으로 다시 구현한 예제 코드를 보겠습니다.

    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 obj) {
            return s.equals(obj);
        }
    
        @Override
        public int hashCode() {
            return s.hashCode();
        }
    
        @Override
        public String toString() {
            return s.toString();
        }
    }

     우선 전달 클래스입니다. 전달 클래스는 Set 인터페이스를 구현하여 Set의 기능들을 정의하도록 하였고, 컴포지션을 이용하여 , Set의 구현체를 인스턴스로 참조하도록 private 필드를 정의하였습니다. 이 클래스는 Set의 기능들을 이용할 수 있도록 기능들을 전달하는 역할을 하고, Set에 또 다른 기능을 추가하여 새로운 Set을 구현할 수 있도록 해줍니다.

     

    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;
        }
    }
        @Test
        public void 컴포지션을_활용한_예제() {
            InstrumentedSet<String> s = new InstrumentedSet<>(new TreeSet<>());
            s.add("하나");
            s.add("둘");
            s.add("셋");
            s.addAll(Arrays.asList("넷", "다섯", "여섯"));
            System.out.println("this count is : " + s.getAddCount());
        }
    this count is : 6

     위에서 보았던 전달 클래스를 상속하여 확장한 래퍼 클래스입니다. 이 클래스에는 아까와 같이 원소가 몇 개가 추가되는지 카운트 하는 기능을 가지고 있습니다.  이러한 구조는 기존 클래스에 새로운 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 부릅니다.

      상속을 할 때는 반드시 하위 클래스가 상위 클래스와 is-a 관계인지 생각해봐야 합니다. 만약 그렇지 않다면 상속 대신 컴포지션과 전달 메서드를 이용하는 편이 좋습니다.

     

     

    Decorator Pattern

    참고용 데코레이터 패턴 구조

     


    참고 자료

     

    Effective Java 3rd (인사이트 출판)

    반응형

    댓글

Designed by Tistory.