Use Composition instead of Inheritance

jiho·2021년 6월 10일
0

EffectiveJava

목록 보기
12/12
post-thumbnail

Effective Java 3판 Item18 상속보다는 컴포지션을 사용하라

흔히 OOP를 공부하다보면 상속을 사용하면 코드 재사용성을 높일 수 있다는 내용을 많이 접하게됩니다. 상속을 잘못 사용할 경우 오류를 내기 쉬운 소프트웨어를 만들게 됩니다.

여기서 말하는 구현 상속을 말합니다. (interface 상속이 아닌 클래스 상속)

상위클래스와 하위클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법입니다.
그리고 확장할 목적으로 설계되었고 문서화도 잘 된 클래스도 마찬가지로 안전합니다.

하지만 일반적인 구체 클래스를 패키지 경계를 넘어, 즉 다른 패키지의 구체 클래스를 상속하는 일은 위험합니다. 다른 사람들이 작성한 패키지를 재사용할 때에서 문제가 발생합니다.

즉, 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있습니다.

다른 개발자들에 의해 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있습니다. 그러므로 인해 하위 클래스가 컴파일 자체가 안되거나 더해서 컴파일은 되지만 오동작할 수도 있습니다. 이러한 이유로 상위 클래스 설계자가 확장을 충분히 고려하고 문서화를 제대로 해두지 않으면 하위 클래스는 상위 클래스의 변화에 매번 수정을 해줘야합니다.

이러한 문제를 발생시키는 원인은 상속이 캡슐화를 깨뜨리기 때문입니다. 그로인해 정보은닉에 실패해서 사용자들이 무분별한 사용이 가능했기 때문에 문제가 발생합니다.

상속으로 발생하는 문제

상속을 잘못 사용해서 하위 클래스가 깨지기 쉬운 이유를 굳이 뽑자면 두 가지 정도가 있습니다.

  • 자신의 다른 부분을 사용하는 자기사용(self-use)여부는 해당 클래스의 내부 구현방식에 해당하며, 사용하는 클라이언트 개발자는 알길이 없다.

템플릿 메서드 패턴이 대표적인 형태입니다. 상위 클래스에서 특정 클래스를 어떻게 사용할지는 공개되지않기 때문에 클라이언트에서 제대로된 내용을 모른체 Override를 할 경우, 예측할 수 없는 상황이 발생할 여지가 있습니다.

  • 상위 클래스에 어떤 시그니처의 메서드가 새로 추가될지 알 수 없다. 즉, 여러분이 하위 클래스 내에서 만든 메서드는 상위 클래스가 요구하는 규약을 만족하지 못할 가능성이 크다.

하위 클래스에서 새로운 클래스를 정의해서 사용하다가 상위 클래스에서 필요에 의해 추가된 새로운 메서드의 시그니처가 하위 클래스와 같은 경우, 하위 클래스를 모두 수정해야합니다. 상속이 한번만 이루어졌으면 모르지만 2개 이상의 상속으로 계층적인 구조를 가지고 있다면 수정할 코드가 상당히 많아집니다.

Composition 구성을 활용

다행히 위의 문제들을 모두 피해가는 묘안이 있습니다. 문제의 원인이었던 상속은 캡슐화를 깨트린다는 점을 주목해서 기존 클래스(상위 클래스)를 확장하는 대신 새로운 클래스를 만들고 private 필드로 기존 인스턴스를 참조하게 하는 방법입니다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션(Composition: 구성)라고 합니다.

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

그 결과 새로운 클래스에 새로운 메서드가 추가되는 것에 영향을 받지않습니다. 그리고 상위 클래스의 인터페이스가 바뀌지 않는한 유지보수 측면에서 매우 우수한 설계가 될 것입니다.

이러한 컴포지션 방식과 여러 개발 용어들이 존재합니다.

Decorator pattern in GOF design pattern

Composition 형태로 기존 클래스를 감싸고 부가적인 기능을 추가해서 포워딩해주는 패턴은 데코레이터 패턴이라 부릅니다. GOF의 디자인 패턴 중 하나입니다. 구현은 어떻게 하느냐에 따라 달라지겠지만 컴포지션을 활용하는 것은 모두 동일합니다.

Delegation (위임)

흔히 개발을 하다가 위임한다. delegation이라는 표현이 많이 등장하는데 Composition과 Forwarding의 조합은 넓은 의미로 위임(delegation)이라고 부릅니다. 단, 엄밀히 따지면 내부객체가 Wrapper 객체에 자기 자신의 참조를 넘기는 경우에만 위임에 해당합니다. (번역서에는 거꾸로 되어있음.)

그러면 상속은 어떻게 써야하나?

상속은 반드시 하위 클래스가 상위 클래스의 '진짜' 하위 타입인 상황에서만 쓰여야합니다. 다르게 말하면, 클래스 B가 클래스 A와 is-a 관계일 때만 클래스가 A를 상속해야합니다.

클래스 A를 상속하는 클래스 B를 작성하려 한다면 "B가 정말 A인가?"라고 자문해보는 것이 좋습니다. 그렇다고 확신할 수 없다면 B는 A를 상속해서는 안됩니다. 대답이 아니다라면 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야하는 상황이 대다수입니다. 즉, A는 B의 필수 구성요소가 아니라 구현방법 중 하나 일뿐입니다.

자바 플랫폼 라이브러리에서도 이 원칙을 명백히 위반하는 클래스를 찾을 수 있습니다.

  • Vector를 상속한 Stack 클래스.

Vector클래스를 상속할 경우, Stack은 항상 thread safe한 자료구조가 되어버려서 thread에 안전한 환경에서는 성능이 저하될 수도 있습니다. 그래서 ArrayList를 통해서도 Stack을 구현할 수 있기 때문에 상속보다는 Stack클래스에서 Abstract 클래스를 컴포지션하는 방식이 좋은 설계일 것입니다.

  • Hashtable을 확장한 Properties 클래스.

Properties은 속성목록을 나타내기때문에 엄연히 Hashtable은 아니기 때문에 상속의 잘못된 사용이라 볼 수 있습니다.

컴포지션 대신 상속을 사용하기로 결정하기 전에 마지막으로 자문해야 할 질문은 "확장하려는 클래스의 API에 아무런 결함이 없는가? 결함이 있다면, 이 결함이 여러분 클래스의 API까지 전파돼도 괜찮은가?"입니다. 컴포지션으로는 이러한 결함을 숨기는 새로운 API를 설계할 수 있지만 상속은 상위 클래스의 API를 "그 결함까지도" 그대로 승계하게 됩니다.

정리

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

0개의 댓글