상속을 고려한 설계와 문서화란?
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
[Why]
- 상속을 염두에 두지 않고 설계했고, 상속할 때의 주의점도 문서화해놓지 않은 '외부' 클래스를 상속할 때는 위험하다. 아이템18
- 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않는다.
-
public class Super { // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다. public Super() { overrideMe(); } public void overrideMe() { } } public final class Sub extends Super { // 초기화되지 않은 final 필드. 생성자에서 초기화한다. private final Instant instant; Sub() { instant = Instant.now(); } // 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다. @Override public void overrideMe() { System.out.println(instant); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
- 이 프로그램이 instant를 두 번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다.
- Cloneable과 Serializable 인터페이스를 구현하는 클래스를 상속할 수 있게 설계하는 것은 안 좋은 방법이다.
- clone과 readObject 메서드는 생성자와 비슷한 효과를 내기 때문에, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능한 메서드를 호출해서는 안 된다.
- clone의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.
- readObject의 경우 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의한 메서드부터 호출하게 된다.
[When]
- 상속용으로 설계하지 않은 클래스는 상속을 금지하라.
[How]
- 상속을 금지하는 두 가지 방법
- 클래스를 final로 선언
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 것
- 클래스의 동작을 유지하면서 재정의 가능 메서드를 사용하는 코드를 제거할 수 있는 기계적인 방법
- 각각의 재정의 가능 메서드는 자신의 본문 코드를 private '도우미 메서드'로 옮기고, 이 도우미 메서드를 호출하도록 수정
- 그 다음 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정
- 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 재정의 가능한 메서드를 호출한다면 그 사실을 API 설명에 적시해야 한다.
- API 문서의 메서드 설명 끝에서 종종 "Implementation Requirements"로 시작하는 절을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳이다.
- 이 절은 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.
- @implSpec 태그를 활성화하려면 명령줄 매개변수로 -tag "implSpec:a:Implementation Requirements:"를 지정해주면 된다.
- 문서에 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
- 문서로 내부 메커니즘을 남기는 것만이 상속을 위한 설계의 전부는 아니다. 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
e.g.) java.util.AbstractList의 removeRange 메서드- ... (전략)
이 리스트 혹은 이 리스트의 부분리스트에 정의된 clear 연산이 이 메서드를 호출한다. 리스트 구현의 내부 구조를 활용하도록 이 메서드를 재정의하면 이 리스트와 부분리스트의 clear 연산 성능을 크게 개선할 수 있다.
... (후략) - List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다. 그럼에도 이 메서드를 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.
- 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까?
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.
상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
- ... (전략)
- Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다. private으로 선언한다면 하위 클래스에서 무시되기 때문이다.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
골격 구현(skeletal implementation) 클래스란? (0) | 2022.02.04 |
---|---|
[아이템 20] 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.02.04 |
[아이템 18] 상속보다는 컴포지션을 사용하라 (0) | 2022.01.25 |
[아이템 17] 변경 가능성을 최소화하라 (0) | 2022.01.09 |
[아이템 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.01.08 |