Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라!
상속을 고려한 문서화
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- 호출되는 메서드가 재정의 가능 메서드일 경우, 그 사실을 API 설명에 적시한다.
- 어떤 순서로 호출하는지 적시한다.
- 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
- “좋은 API 문서란 ‘어떻게’가 아닌 ‘무엇’을 하는지 설명해야 한다”는 격언과 대치됨.
- 상속이 캡슐화를 해치기 때문에, 안전한 상속을 위해 어떻게를 기입해야 하기 때문이다.
Implementation Requirements
- 메서드 내부 동작 방식을 설명하는 곳
- 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.
상속을 고려한 설계
- 클래스 내부 동작 과정 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드 형태로 공개해야 할 수 있다.
- 효율적인 하위 클래스를 큰 어려움 없이 만들기 위해
- 상속용 클래스 설계 시 어떤 메서드를 protected로 노출해야 할까?
- 정답은 없다.
- 심사숙고해서 잘 예측해보고, 실제 하위 클래스를 만들어 시험해보자.
- 상속의 이점을 해치지 않는 선에서 가능한 한 protected로 공개된 메서드는 적게 구성한다. (내부 구현 공개에 해당하므로)
- 널리 쓰일 클래스를 상속용으로 설계한다면, 문서화한 내부 사용 패턴 및 protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 한다.
- 그러니 반드시 상속용 설계 클래스를 배포 전 하위 클래스로 검증해야 한다.
- 클래스를 상속용으로 설계하려면 엄청난 노력이 들고, 제약도 상당하다.
- 절대 가볍게 생각하고 상속용 클래스 설계에 임해서는 안된다.
- 추상 클래스나 인터페이스의 골격 구현처럼 상속을 허용하는게 명백히 정당한 상황이 존재한다.
- 불변 클래스처럼 상속용 설계를 하는 것이 명백히 잘못된 상황이 존재한다.
상속을 허용하는 클래스가 지켜야할 제약
상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
어기면 프로그램이 오작동한다.
상위 클래스의 생성자는 하위 클래스의 생성자보다 먼저 호출된다.
하위 클래스에서 재정의한 메서드가 상위 클래스 생성 시 하위 클래스 생성보다 먼저 호출된다.
호출한 재정의 메서드가 하위 클래스 생성자에서 초기화하는 값에 의존하는 경우, 문제가 생긴다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
public class Super { public Super() { overrideMe(); // 재정의 가능한 메서드를 호출 } public void overrideMe() { } } public final class Sub extends Super { private final Instacnce instacnce; Sub() { instacnce = Instacnce.now(); } // 재정의 가능한 메서드. 상위 클래스의 생성자가 호출하고 있음. @Override public void overrideMe() { System.out.println(instacnce); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
- instance를 두 번 출력하길 기대했으나, 처음은 null을 , 두 번째는 instance를 출력한다.
- instance 생성 전 접근했으니 원래는 NullPointerException을 던질 것이다. 다만 println이 null 입력도 받기에 null을 출력했다.
- instance를 두 번 출력하길 기대했으나, 처음은 null을 , 두 번째는 instance를 출력한다.
Cloneable, Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다.
- 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋은 생각이 아니다.
- clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다.
- 새로운 객체를 생성
- readObject : 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의 메서드를 호출함.
- clone : 하위 클래스의 clone 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의 메서드를 호출함.
- 이는 복제본 뿐만 아니라 원본에도 영향을 미칠 수 있다.
- 생성자와 비슷한 효과를 내기에 제약도 생성자와 비슷하다.
- clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
일반 구체 클래스의 경우
- 전통적으로 final도 아니고, 상속용으로 설계되지도, 문서화 되지도 않았다.
- 그대로 두면 하위 클래스 오작동을 유발할 위험이 존재한다.
문제 해결
- 상속용으로 설계하지 않은 클래스는 상속을 금지한다.
- final로 설계한다.
- 모든 생성자를 private이나 package-private으로 선언하고, public 정적 팩터리를 만들어준다.
- 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지한다면, 사용하기에 상당히 불편할 것이다.
- 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 이 사실을 문서로 남긴다.
핵심 정리
- 상속용 클래스 설계는 만만치 않으니 잘 고려하라.
- 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남기자.
- 문서화 한 내용은 그 클래스가 쓰이는 한 반드시 지켜야 한다.
- 지키지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스의 오작동을 유발한다.
- 효율 좋은 하위 클래스의 추가 생성을 위해 일부 메서드를 protected로 제공해야 할 수 있다.
- 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하라.
- 상속을 금지하려면 final로 선언하거나, 생성자의 외부 접근을 차단하라.
Comments powered by Disqus.