Item 18. 상속 대신 컴포지션을 사용하라

상속의 위험성

상속

class MySet<E> extends HashSet<E> {

    @Override
    public int size() {
        return super.size();
    }

}

상속은 캡슐화를 깨트림

  • 상위 클래스의 내부 구현이 달라졌을 때, 이 클래스의 API만을 이용하는 외부 클라이언트는 영향을 받지 않음

  • 그러나 이를 상속하는 하위 클래스는, 내부 구현 변경에 의해 영향을 받을 수 있음

    • 왜? 하위 클래스에서는 상위 클래스를 오버라이딩할 수 있고, 상위 클래스에서 내부 구현 상 내부의 메서드를 호출하는 과정에서 다른 내부 메서드를 호출하게 되면 오버라이딩된 메서드를 호출하게 됨

    • 따라서 자기사용(self-use) 여부에 따라 동작이 달라질 수 있으며, 이는 내부 구현 방식에 해당

  • 또한, 상위 클래스에 새로운 메서드가 추가되거나 하는 이유로 동작이 달라질 수 있음

해결책과 뒤따르는 문제점들

  • 상위 클래스의 메서드를 오버라이딩하는 과정에서 super를 호출하는 대신 아예 동작 자체를 재정의할 수 있으나, 이는 복잡하고 성능 저하를 일으킬 수 있음

  • 아예 이름만 같고 반환 타입/매개 인자가 다른 메서드를 새로 선언할 수 있으나, 이 역시 아래의 문제점들을 야기

    • 상위 클래스의 새 릴리스에 시그니처가 같고 반환 타입이 다른 메서드가 추가됨 - 생성한 메서드가 무시됨

    • 상위 클래스의 새 릴리스에 시그니처, 반환 타입이 모두 같은 메서드가 추가됨 - 단순 오버라이딩처리되어 오동작 위험이 있음

문제 요인

  • 하위 클래스의 재정의가 상위 클래스의 동작에도 영향을 미치게 되기 때문에 내부 구현에 영향을 받게 됨

  • 상위 클래스와 하위 클래스는 강하게 연결되며, 상위 클래스의 새 변경점에 하위 클래스는 큰 영향을 받음

  • 이는 하위 클래스가 사실은 상위 클래스와 is-a 관계가 아닌데 상속하기 때문에 주로 발생하는 문제들로, 이러한 경우엔는 상속 대신 컴포지션(Composition, 합성)을 이용해야 함

컴포지션

class MySet<E> implements Set<E> {

    private final Set<E> set;

    // Set 인터페이스의 메서드들을 구현
    // 포워딩 방식을 이용
    @Override
    public int size() {
        return set.size();
    }

}
  • 기존 클래스를 확장하는 대신, 이 클래스의 인스턴스를 내부 필드로 참조하게 하는 설계

  • 새 클래스는 기존 클래스의 메서드를 호출하게 되고, 이를 처리를 넘긴다는 의미로 위임(delegation)이라고 부름

  • 또한, 새 클래스는 다른 인스턴스를 감싸는 역할을 한다는 뜻에서 래퍼(Wrapper) 클래스라고 부름

  • 위와 같이 컴포지션을 이용하면, Set 인터페이스를 구현한 구체 클래스를 상속하는 대신 어떤 Set의 기능이라도 유연하게 이용이 가능

  • MySet은 Set에 특정한 기능을 덧씌울 수 있는 클래스라는 점에서 데코레이터(Decorator) 패턴을 이용했다고 볼 수 있음

언제 상속을 사용?

  • 하위 클래스가 상위 클래스와 정확히 is-a 관계일 때에만 상속을 이용할 수 있음

    • 리스코프 치환 원칙(서브 타입은 언제나 기반 타입으로 교체할 수 있어야 한다) 에서 또한 이 점을 강조하고 있음

    • 이를 만족하는 방법은, 하위 클래스를 상위 클래스로 대체했을 때 모든 동작이 정상적인가? 를 보면 됨

  • 그러나 상속을 이용하면 하위 클래스는 상위 클래스의 결함을 그대로 물려받게 되며, 상위 클래스가 확장을 고려해 설계되지 않았다면 오버라이딩한 메서드에 의해 다양한 문제가 발생할 수 있음

  • 따라서 상속 대신 컴포지션을 사용하는 편이 더 유연한 클래스를 정의할 수 있음

Last updated