상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게 된다.
상위 클래스와 하위 클래스를 모두 같은 프로그래머가 통제하는 패키지 안에서라면 상속도 안전한 방법이다.
일반적인 구체 클래스를 패키지 경계를 넘어 다른 패키지의 구체 클래스를 상속하는 일은 위험하다.
상속의 문제점
메서드 호출과 달리 캡슐화를 깨뜨린다.
상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있다.
->릴리스마다 내부 구현이 달라질 수 있는 상위 클래스 때문에 건드리지 않은 하위 클래스가 오동작할 수 있다.
HashSet을 상속받아 set에 추가한 원소의 개수를 저장할 수 있게 하는 클래스 InstrumentedSet을 만들어보자.
3개의 원소를 더했으므로 s.addCount는 3이 출력되길 원했지만
6이 출력되었다. 왜 이렇게 된 걸까?
바로 InstrumentedSet의 상위 클래스인 Set의 addAll() 메서드 때문이다.
이 메서드는 같은 클래스 메서드인 add()를 사용해 구현했는데, 이를 상속받은 InstrumentedSet의 addAll()도 add()를 호출했다. 하지만 Set 클래스의 add()를 호출한 것이 아니고 addAll()과 같이 오버라이딩한 add()를 호출해 버려서 한 번씩 더 addCount++를 해주게 된 것이다.
해당 클래스의 문제는 우리가 상위 클래스의 메서드 구현 방식을 알고 있기 때문에 파훼할 수 있지만 다른 문제에 마딱뜨렸을때는 쉽지 않을 것이다.
보안 때문에 컬렉션에 추가된 모든 원소가 특정 조건을 만족해야만 할 때
해결 방법 : 컬렉션을 상속하여 원소를 추가하는 모든 메서드를 오버라이딩해 필요조건을 검사하게 하자.
-> 상위 클래스에 또 다른 원소 추가 메서드가 만들어지기 전까지만 유효하다. 상위 클래스에 새로운 추가 메서드가 만들어지면 하위 클래스에서 미처 재정의하지 못한 추가 메서드를 이용해 허용되지 않은 원소를 추가할 수 있다.
해결법
위 두 문제 모두 메서드 오버라이딩이 원이이었다.
따라서 메서드 오버라이딩 대신 새로운 메서드를 추가한다면 다음과 같은 문제점이 발생한다.
- 상위 클래스에 새 메서드가 추가되었을 때, 우연히 우리가 하위 클래스에 추가한 메서드와 시그니처가 같고 반환 타입은 다르다면 우리의 클래스는 컴파일 조차 되지 않는다.
- 우리가 메서드를 추가할 때 상위 클래스의 메서드는 존재하지도 않았으니 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 가능성이 크다.
이러한 문제를 모두 피해 가는 묘안이 있는데 바로 컴포지션이다.
컴포지션이란 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방식이다. 기존 클래스가 새로운 클래스의 구성요소로 쓰이는 것이다.
새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환하는데 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 부른다.
InstrumentSet은 HashSet의 모든 기능을 정의한 Set 인터페이스를 활용해 설계되어 견고하고 아주 유연하다.
임의의 Set에 계측 기능을 덧씌워 (새로운 기능을 추가해) 새로운 Set을 만드는 것이 핵심이다.
상속 방식은 구체 클래스 각각을 따로 확장해야 하며, 지원하고 싶은 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해줘야 한다. 또, 한 번만 구현해 두면 어떠한 Set 구현체라도 계측할 수 있으며 기존 생성자들과도 함께 사용할 수 있다.
다른 Set 인스턴스를 감싸고 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라고 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코리에터 패턴이라고 한다.
단점
래퍼 클래스가 콜백 프레임워크와는 어울리지 않는다.
상속할 때 고려해야 할 점
상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다.
클래스 A를 상속하는 클래스 B를 작성하려 할 때 B가 정말 A인가?라는 질문에
맞다고 대답할 수 없다면 상속하지 말자->컴포지션 방식을 쓰자.
맞다고 대답한다면 상속을 하자!.
만약 컴포지션 대신 상속을 사용하기로 결정하기 전에 생각해 보자.
- 확장하려는 클래스의 API에는 아무런 결함이 없는가?
- 만약 결함 있다면 그 결함이 다른 클래스의 API까지 전파돼도 괜찮은가?
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2023.05.03 |
---|---|
[Effective Java] Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) | 2023.03.28 |
[Effective Java] Item 17. 변경 가능성을 최소화하라 (0) | 2023.03.25 |
[Effective Java] Item 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (2) | 2023.03.21 |
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.03.21 |