상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
앞서 item 18에서 HashSet 클래스를 상속하여 add를 재정의 한 것이 addAll 까지 영향을 준다는 사실을 알 수 없었다. 이러한 문제점을 문서화하여 주석처리만 해줘도 add 메서드로 내부 구현이 되있구나 라는 것을 알 수 있다.
좋은 문서화는 무엇일까?
문서화를 좋게 작성 하려면 어떻게가 아닌 무엇을 하는지를 설명해야한다. 즉, 내부 구현 방식을 설명해야 한다.
효율적인 하위 클래스를 만들려면?
클래스 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선변하여 protected 메서드 형태로 공개해야 할 수 도 있다.
java.util.AbstractList 의 removeRange 메서드를 예로 살펴보자.
List 구현체의 초종 사용자는 removeRange 메서드에 관심이 없다. 그럼에도 이 메서드를 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.
removeRange가 없다면 하위클래스에서 clear 메서드를 호출하면 제곱에 비례해 성능이 느려지거나 부분리스트의 메커니즘을 새로 구현해야 했을 것이다.
어떤 메서드를 protected 로 할지 어떻게 결정할까?
실제 하위 클래스를 만들어 시험해보는 것이 최선이다. protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 적어야 한다. 너무 적게 노출해서 상속으로 얻는 이점마저 없애지는 말자.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다. 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
상속을 허용하는 클래스가 지켜야 할 제약
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public class Super {
// 생성자가 재정의 가능 메서드를 호출한다.
public Super() {
overrideMe();
}
public void overrideMe() {
System.out.println("재정의 가능 메서드");
}
}
위의 클래스를 사용하여 하위 클래스를 구현
public final class Sub extends Super {
// 초기화되지 않은 final 필드, 생성자에서 초기화한다.
private final Instant instant;
Sub() {
this.instant = Instant.now();
}
@Override
public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
- 예상 되는 출력 결과는 instant를 2번 출력하리라 기대했지만 null 이 한 번 나오고 instant가 한 번 나온다.
- 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
- overrideMe에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe를 호출할 때 NullPointerException을 던지게 된다. 하지만 println()이 null 입력도 받아들여 null 출려이 나온 것이다.
- private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
clone 과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
- cloneable과 serializable 인터페이스들을 구현할 때 따르는 제약도 생성자와 비슷하다는 점에 주의하자.
- readObject의 경우 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의한 메서드부터 호출하게 된다.
- clone의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출하게 된다.
- clone이 잘못되면 복제본뿐 아니라 원본 객체에도 피해를 줄 수 있다.
Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 proteteced로 선언해야 한다.
- private으로 선언하면 하위 클래스에서 무시되기 때문이다.
- 상속을 허용하기 위해 내부 구현을 클래스 API로 공개하는 예 중 하나다.
상속용으로 설계하지 않은 클래스는 금지하는 것이 좋다
금지하는 2가지 방법
- 클래스를 final 로 선언하는 방법
- 모든 생성자를 private나 default로 선언하고 public 정적 팩터리를 만들어주는 방법이다.
- 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성 제공
상속을 허용해야겠다면?
- 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남기는 것이다.
- 재정의 가능 메서드를 호출하는 자기 사용 코드를 완벽히 제거
- 재정의 가능 메서드는 자신의 본문 코드를 private 도우미 메서드로 옮기고, 이 도우미 메서드를 호출하도록 수정
- 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정 한다.
정리
- 상속용 클래스를 만들면 클래스 내부에서 스스로 어떻게 사용하는지 모두 문서로 남겨야 한다.
- 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다.
- 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만든다.
'Book > Effective Java' 카테고리의 다른 글
[Item 23] 태그 달린 클래스보다 클래스 계층구조를 활용하라 (0) | 2022.07.27 |
---|---|
[Item 22] 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2022.07.26 |
[Item 21] 인터페이스는 구현하는 쪽을 생각해 설계해라 (0) | 2022.07.26 |
[Item 20] 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.07.26 |
[Item 18] 상속보다는 컴포지션을 사용하라 (0) | 2022.07.22 |
[Item 17] 변경 가능성을 최소화 하라 (0) | 2022.07.21 |
[Item 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.07.21 |
[Item 15] 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2022.07.20 |