클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출 할 수도 있다.
이 공개된 메서드가 *재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API설명에 어떤 순서로 호출하는지 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
재정의 가능이란 public과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.
이렇듯 클래스를 안전하게 상속할 수 있도록 하려면 내부 구현 방식을 설명해야 한다.
또는 클래스의 내부 동작 과정 중간에 호출될 수 있는 메서드(재정의 가능 메서드, hook 메서드)를 선별하여 protected같은 접근제어자로 공개해야할 수도 있다. 예로 java.util.AbstracList의 removeRange 메서드가 있다.
상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어 보는 것이 유일하다. 꼭 필요한 protected멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확실히 드러나고, 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않은 protected멤버는 사실 private이였어야할 가능성이 크다.
이 규칙을 어길 시 프로그램이 오동작할 가능성이 매우 크다. 다음은 이 규칙을 어기는 코드다.
public class Super {
//생성자가 재정의 가능 메서드를 호출(잘못된 예)
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
다음과 같이 하위 클래스에서 overrideMe메서드를 재정의 하면 오동작을 일으킨다.
public class Sub extends Super{
private final Instant instant;
public 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();
}
}
//출력 결과
null
2020-06-30T08:42:50.885284Z
이 프로그램이 instant를 두 번 출력할거라 예상했지만, 첫 번째는 null을 출력한다.
상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
private, final, static메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
결론적으로는 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하다. 그 외의 일반적인 구체 클래스 또한 final도 아니고 상속용으로 설계되거나 문서화되지 않았다. 이 문제를 해결하기 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.
-
클래스를 final로 선언하는 방법
-
모든 생성자를 private, package-private으로 선언하고 pulbic 정적 팩터리를 만들어주는 방법
상속용 클래스를 설계하기 위해선 클래스 내부에서 스스로 어떻게 사용하는지(자기사용 패턴)모두 문서로 남겨야 한다. 그리고 그 클래스가 쓰이는 한 반드시 지켜야 한다.
또한 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드(ex removeRange)를 protected로 제공해야 할 수도 있다.
그러니 상속용 클래스를 만들지 않는 것이 나으며, 상속을 금지하려면 final이나 정적 팩터리 메서드를 생성해서 외부에서 접근할 수 없도록 만들면 된다.