상속을 고려한 설계
item 18에서 상속에서 어떤 문제가 생길 수 있는지 알아봤다. 그럼에도 불구하고 상속을 사용하려면, 상속용 부모클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용)에 대해 문서로 남겨야 한다. 어떤 순서로 호출하고, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
Implementation Requirements
API 문서의 메서드 설명에서 Implementation Requirements 라는 절이 있다. 메서드 주석에 @implSpec 태그를 붙이면 자바독 도구가 생성해준다. 여기에 재정의 했을 때 발생할 수 있는 내용을 담는 경우가 많다.
하지만 좋은 API문서는 어떻게 하는지가 아니라 무엇을 하는지를 적어야 한다. 상속이 캡슐화를 해치기 때문에 안전하게 상속할 수 있게하기 위한 어쩔 수 없는 선택이다.
hook을 만들기
효율적인 하위 클래스를 만들 수 있게 하려면 클래스 내부 동작 과정에 끼어들 수 있는 훅(hook) 메서드를 잘 선별하여 protected 메서드 형태로 공개해야 할 수 도 있다.
protected 메서드와 필드는 공개 API이기 때문에 영원히 책임져야 한다. 그러므로 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해보자.
상속 시 주의점
1.
상속용 클래스의 생성자는 재정의 가능한 메서드(non-private, non-final, non-static)를 호출하면 안 된다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의 해버린 메서드가 하위 클래스 생성자보다 먼저 호출된다. 이 때 하위 생성자에서 초기화 하는 값에 의존한다면 의도대로 동장하지 않을 것이다.
public class Super {
// Broken - constructor invokes an overridable method
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
Java
복사
재정의 가능한 메서드를 호출하는 상위 클래스의 생성자
public final class Sub extends Super {
// Blank final, set by constructor
private final Instant instant;
Sub() {
instant = Instant.now();
}
// Overriding method invoked by superclass constructor
@Override public void overrideMe() {
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
Java
복사
출력 결과는 null, now이다! final 필드임에도 불구하고!
2.
Cloneable과 Serializable 인터페이스는 상속용 설계를 어렵게 한다.
둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다. clone과 readObejct 메서드도 생성자 처럼 기능할 수 있기 때문에, 재정의한 메서드를 호출해서는 안 된다.
지켜야 할 원칙
상속을 고려하지 않은 구체클래스는 상속을 금지하자. final class로 만들거나, 생성자를 private 나 package-private로 선언하고 정적 팩토리 메서드를 활용하자.
상속을 통한 확장보다는, 핵심 기능을 정의한 인터페이스가 있고 클래스가 그 인터페이스를 구현하도록 하자. List, Set, Map이 좋은 사례이다.
인터페이스를 구현하지 않은 구현체의 경우 이런 제약이 불편하기 때문에, 재정의 가능한 메서드를 줄이기 위해 자기사용하는 public 메서드를 private 메서드로 대체하자.
용어정리
한글명 | 영어명 |
금지 | prohibit |
성명 | dictum |