아이템 18. 상속보다 컴포지션을 사용하라

문법식·2022년 5월 4일
0

Effective Java 3/E

목록 보기
18/52

상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉽다. 상위 클래스와 하위 클래스가 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다. 확장할 목적으로 설계되었고 문서화도 자 된 클래스도 마찬가지로 안전하다. 하지만 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.

메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 상위 클래스가 어떻게 구현되는냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다. 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며, 그 여파로 코드 한 줄 건드리지 않은 하위 클래스가 오동작할 수 있는 것이다.
예를 들어 상위 클래스가 자신의 다른 부분을 사용하는 클래스일 때, 하위 클래스가 상위 클래스의 자신의 다른 부분을 사용하는 함수와 그 사용되는 함수를 재정의할 경우 오류가 발생할 수 있다. 상위 클래스가 자신의 다른 부분을 사용하는 자기사용 해당 클래스의 내부 구현 방식에 해당하고, 다음 릴리스에서 해당 구현이 유지될지 알 수 없기 때문이다.

상위 클래스에 새로운 메서드가 추가될 경우 하위 클래스가 깨질 수 있다. 보안 때문에 컬렉션에 추가된 모든 원소가 특정 조건을 만족해야만 하는 프로그램을 예시로 들어본다. 하위 클래스에서 원소를 추가하는 모든 메서드를 재정의해 조건 검사를 하면 될 것 같다. 하지만 다음 릴리스에서 상위 클래스에 새로운 원소 추가 함수가 생기면 보안은 깨지고 만다. 하위 클래스에서 재정의하지 못한 상위 클래스의 함수로 허용되지 않은 원소가 추가될 수 있다.

위의 두 문제는 모두 메서드 재정의가 원인이었다. 따라서 클래스를 확장할 때 메서드를 재정의하는 대신 새로운 메서드를 추가하면 괜찮다고 생각할 수 있다. 이 방식이 훨씬 안전한건 맞지만, 위험이 전혀 없는 것은 아니다. 다음 릴리스에서 상위 클래스에 새 메서드가 추가됐는데, 하위 클래스에서 추가한 메서드와 시그니처가 같고 반환 타입이 다르면 우리가 만든 클래스는 컴파일조차 되지 않는다.

위의 모든 문제를 피해가는 방법이 있다. 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하는 것이다. 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 뜻에서 이러한 설계를 컴포지션이라 한다. 새 클래스의 인스턴스 메서드들은(private 필드로 참조하는) 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다. 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들은 전달 메서드(forwarding method)라 부른다. 그 결과 새로운 클래스는 기존 클래스의 내부 구현 방식의 영햐에서 벗어나며, 심지어 기존 클래스에서 새로운 메서드가 추가되더라도 전혀 영향받지 않는다.

래퍼 클래스는 단점이 거의 없다. 한 가지, 래퍼 클래스가 콜백 프레임워크와는 어울리지 않는다는 점만 주의하면 된다. 콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 한다. 내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신(this)의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. 이를 SELF 문제라고 한다. 전달 메서드가 성능에 주는 영향이나 래퍼 객체가 메모리 사용량에 주는 영향은 거의 없는 것으로 실전에서 밝혀졌다.

상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다. 클래스 B가 클래스 Ais-a 관계일 때만 클래스 A를 상속해야 한다. 클래스 A를 상속하는 클래스 B를 작성하려 한다면 "B가 정말 A인가?" 고민해보자. 확신할 수 없다면 BA를 상속해서는 안 된다. 고민 결과 "아니다"라면 Aprivate 인스턴스로 두고 A와는 다른 API를 제공해야 하는 상황이 대다수다.

profile
백엔드

0개의 댓글