2 minute read

item 19. 상속을 고려해 설계하고 문서화하고, 그러지 않았다면 상속을 금지해라

상속을 고려한 문서화

상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.

  • 다시 말해, 재정의 가능한 메서드를 호출할 수 있는 모든 상황을 문서화해야 한다.
  • 재정의 가능한 메서드 : publicprotected 메서드 중 final이 아닌 모든 메서드

@implSpec 태그

  • 메서드 주석에 붙여주면 자바독 도구가 내부 동작 방식을 설명하는 절을 생성해준다.
  • 이 절은 “Implementation Requirements”로 시작하게 된다.
  • 이 태그를 활성화하려면, 자바 프로그램 실행 시 명령줄 매개변수로 -tag "implSpec:a:Implementation Requirents:"를 지정해주면 된다.

protected 필드 및 메서드

클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 혹은 필드로 형태로 공개해야 할 수도 있다.

이러한 접근제한자의 결정은 직접 하위 클래스를 만들어 시험해 보는것이 유일한 해법이다.

상속용으로 설계한 클래스는 배포 전 반드시 하위 클래스를 만들어 검증해야 한다. 이 검증을 위한 하위 클래스는 3개 정도가 적당하다.

상속 클래스의 제약과 금지 방법

public class Super {
    // 잘못된 예시 - 생성자가 재정의 가능 메서드를 호출한다.
    public Super() {
        overrideMe(); 
    }

    public void overriedMe() {
    // 이 메서드는 하위 클래스에서 재정의가 가능하다.
    }
}
public final class Sub extends Super {
    // 초기화되지 않은 final 필드, 생성자에서 초기화한다.
    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();
    }
}

  • 이 프로그램은 instance를 두 번 출력하지 않고, 첫 번째는 null을 출력한다.
  • 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.

실행 순서

  1. new Sub() 호출
  2. Super 클래스의 생성자 실행 overrideMe() 호출: 하위 클래스의 생성자가 아직 실행되지 않아 instant은 초기화되지 않아 null 출력
  3. Sub 클래스의 생성자 실행 instant 초기화: Instant.now()를 통해 필드 값 초기화
  4. sub.overrideMe() 호출 overrideMe()에서는 이제 instant이 필드 값이 초기화돼서 인스턴스가 출력됨
  1. 상속용 클래스의 생성자는 직접적/간접적 여부에 관계없이 재정의 가능 메서드를 호출해서는 안 된다.
    • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
    • 이때, 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다. (위 예시 코드 참고)
    • privatefinalstatic 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
  2. Cloneable과 Serializable 인터페이스 둘 중 하나라도 구현한 클래스라면 상속하지 않는 것이 좋다.
    • clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
  3. Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면, 이 메서드들은 private이 아닌 protected로 선언해야 한다.
    • private로 선언한다면 하위 클래스에서 무시되기 때문이다.

클래스를 확장해야 할 명확한 이유가 없다면, 상속을 금지하는 편이 좋다.

  1. 클래스를 final로 선언한다. (더 쉬운 방법)
  2. 모든 생성자를 private이나 package-private으로 선언하고, public 정적 팩터리를 만들어준다.

Leave a comment