상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야한다. 간단히 말하면 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야한다.
[상속 문서화 주요내용]
API 문서의 메서드 설명 끝에서 종종 Implementation Requirements 로 시작하는 절을 볼 수 있는데, 해당 메서드의 내부 동작 방식을 설명하는 곳이다.
이 절은 메서드 주석에 @implSpec
태그를 붙이면 자바독 도구가 자동으로 생성해준다.
[java9 doc - AbstractCollection]
-tag "implSpec:a:Implementation Requirements:"
를 지정해주면 된다.좋은 API문서란 [어떻게] 가 아닌 [무엇] 을 하는지를 설명해야한다. 클래스를 안전하게 상속할 수 있도록 하려면 (상속만 아니었다면 기술하지 않았어야 할) 내부구현 방식을 설명해야만 한다.
효율적인 하위 클래스를 큰 어려움 없이 만들려면 내부 매커니즘을 문서로 남기는것뿐만 아니라 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected
메서드 형태로 공개해야 할 수도 있다.
[java9 doc - AbstractList]
protected
멤버를 놓쳤다면 하위 클래스 작성시 그 빈자리가 드러난다.protected
멤버는 사실 private
이어야 할 가능성이 크다.protected
메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야한다.[생성자가 재정의 가능 메서드를 호출한다.]
public class Super {
public Super() {
override();
} // 생성자가 재정의 가능 메서드 호출
public void override(){
}
}
[override 메서드를 재 정의한 하위 클래스]
public class Sub extends Super{
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
과 instant
를 각각 한번씩 출력한다.
그 이유는 하위 클래스의 생성자가 인스턴스필드를 초기화하기도 전에 상위 클래스의 생성자가 overrideMe
메서드를 호출하기 때문이다.
private
,final
,static
메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
clone
과 readObject
는 생성자와 비슷한 효과를 낸다.Cloneable
이나 Serializable
을 구현할지 정해야 한다면, 이들 구현시 따르는 제약도 생성자와 비슷하다는것을 인식해야 한다.clone
과 readObject
모두 직간접적으로 재정의가능 메서드를 호출해서는 안된다.readObject
의 경우 하위 클래스의 상태가 미처다 역직렬화되기 전에 재정의한 메서드부터 호출하게 된다.clone
의 경우 하위 클래스의 clone
메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.clone
이 잘못 복제되면 복제본뿐 아니라 원본객체에도 피해를 줄 수 있다.Serializable
을 구현한 상속용 클래스가 readResolve
나 writeReplace
메서드를 갖는다면 이 메서드들은 protected
로 선언해야 한다.private
으로 선언되면 하위 클래스에서 무시되기 때문이다.final
도 아니고 상속용으로 설계되거나 문서화되지 않아 그대로 두면 위험하다. 클래스 변화시 오작동이 발생할 가능성이 있기 때문이다. final
로 선언하는 방법private
이나 package-private
으로 선언하고 public
정적 팩터리를 만들어주는 방법만약, 핵심기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 괜찮다.
이렇게 재정의 가능 메서드를 호출하는 자기 사용 코드를 완벽히 제거하면 메서드를 재정의해도 다른 메서드의 동작에 아무런 영향을 미치지 않게된다.
상속용 클래스를 설계하려면 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.
만약, 이를 지키지 않으면 문서를 믿고 활용하던 하위 클래스를 오작동하게 만들 수 있다.
또한, 하위 클래스를 효율적으로 만들수 있도록 protected
접근제어자를 갖는 일부 메서드를 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 사유가 없다면 상속을 금지하는 편이 낫다.
상속을 금지하려면 클래스를 final
로 선언하거나 생성자를 외부에서 접근할 수 없도록 하면된다.
[Reference]
이펙티브자바 아이템19
아이템19 - 상속을 고려해 설계화하고 문서화하라 그러지 않았다면 상속을 금지해라