ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Effective Java] 아이템18: 상속보다는 컴포지션을 사용하라
    Language/Java 2022. 7. 2. 04:45

     

    이번 아이템에서 논하는 상속은 클래스가 다른 클래스를 확장하는 구현 상속을 말한다.
    클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.

     

    ✔️ 상속의 위험성

    메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.

    • 상위 클래스가 릴리스 때 내부 구현이 달라지면, 그 여파로 하위 클래스가 오동작할 수 있다.
    • 메서드를 재정의하는 대신 새로운 메서드를 추가하더라도, 문제가 발생할 수 있다.
      • 상위 클래스의 새 메서드와 시그니처가 같고 반환 타입이 다르다면, 컴파일 오류가 발생한다.
      • 시그니처와 반환 타입 모두 같을 경우, 상위 클래스의 메서드를 재정의한 것이 된다.
      • 새로 추가한 메서드가 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 가능성이 크다.

     

     

    ✔️ 컴포지션 (Composition)

    이러한 문제를 해결하기 위한 방법은 바로 컴포지션이다.

    기존의 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.

    새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
    이 방식을 전달(forwarding)이라고 하고, 새 클래스의 메서드들을 전달 메서드(forwarding method)라고 한다.

     

    컴포지션 방식을 사용하면 인스턴스도 계측할 수 있고, 기존 생성자들도 함께 사용할 수 있다.

    예시 코드

    // 코드 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;
        }
    }
    // 코드 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(); }
    }

     

     

    ✔️ 컴포지션 단점

    컴포지션의 단점은 거의 없다.

    한 가지 주의할 점은 클래스가 콜백 프레임워크와는 어울리지 않는다는 점이다.

    콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출 때 사용하도록 한다.
    내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. (SELF 문제)

     

     

     

     

    핵심 정리

     

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

     

     

     

     

    728x90

    댓글

Designed by Tistory.