반응형

이 글에서 의미하는 상속은 인터페이스를 통한 상속이 아니라 클래스가 다른 클래스를 확장하는 구현 상속이다.

상속은 코드를 재사용하기 위한 수단이기는 하지만 항상 최선의 수단은 아니다. 상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다. 확장할 목적으로 설계되었고 문서화도 잘 된 클래스도 마찬가지로 안전하다. 하지만 클래스를 패키지 경계를 넘어서 다른 패키지의 클래스를 통해 상속하는 일은 지양해야 한다. 

상속은 캡슐화를 깨뜨린다. 상속한 클래스들끼리 각자의 변화에 큰 영향을 받기 때문이다.

상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다. 다르게 말하면, 클래스 B가 클래스 A와 is-a 관계일 때만 클래스 A를 상속해야 한다. 클래스 A를 상속하는 클래스 B를 작성하려 한다면 B가 정말 A인가? 라고 자문해보자. 그렇다고 확신할 수 없다면 B는 A를 상속해서는 안 된다. 그런 경우는 A를 private 인스턴스로 두고, A와는 다른 API를 제공해야 하는 상황이 대다수다. 즉, A는 B의 필수 구성요소가 아니라 구현하는 방법의 하나일 뿐이다.

컴포지션을 써야 할 상황에서 상속을 사용하는 건 내부 구현을 불필요하게 노출하는 꼴이다. 그 결과 API가 내부 구현에 묶이고 그 클래스의 성능도 영원히 제한된다. 더 심각한 문제는 클라이언트가 노출된 내부에 직접 접근할 수 있다는 점이다. 다른 문제는 접어두더라도, 사용자를 혼란스럽게 할 수 있다. 가장 심각한 문제는 클라이언트에서 상위 클래스를 직접 수정하여 하위 클래스의 불변식을 해칠 수 있다는 사실이다. 예를 들어, Properties는 키와 값으로 문자열만 허용하도록 설계하려 했으나, 상위 클래스인 Hash Table의 메소드를 직접 호출하면 이 불변식을 깨버릴 수 있다. 불변식이 한번 깨지면 load와 store 같은 다른 Properties API는 더 이상 사용할 수 없다. 이 문제가 밝혀졌을 때는 이미 수많은 사용자가 문자열 이외의 타입을 Properties의 키나 값으로 사용하고 있었다. 문제를 바로잡기에는 너무 늦어버린 것이다.

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

요약하자면,

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

 

출처: 이펙티브 자바 3판

반응형

+ Recent posts