도서

[도서] Effective Java - Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

땀두 2022. 5. 11. 15:58

이펙티브 자바 3판을 읽으면서 내용을 정리하는 포스트입니다. 혹시 틀린 부분이나 잘 못 설명한 부분이 있으면 댓글로 남겨주시면 수정하도록 하겠습니다.

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

외부 클래스를 상속할 경우 언제 변경될지 모르기 때문에 여러 문제가 발생할 수 있다. 그렇기 때문에 상속용 클래스를 설계 할 때는 문서화가 필요하다.

문서에 남겨야 하는 내용

  • 재정의할 수 있는 메소드들이 내부적으로 어떻게 이용하는지 적어야 한다.
  • API로 공개된 메소드로부터 호출되는 재정의 메소드는 호출된다는 사실과 어떤 순서로 호출되는지와 호출 결과에 이어지는 처리에 어떤 영향을 주는지 적어야 한다.
  • ※ 재정의 가능 메소드 : public(or protected)메소드 중 final이 아닌 모든 메소드

AbstractCollection

효율적인 하위 클래스를 어려움 없이 만들 수 있게 하기 위해 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메소드 형태로 공개해야 할 수도 있다.

상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
  • 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 확연히 드러난다. 또한 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private이었어야 할 가능성이 높다.
  • 이런 하위 클래스는 3개 정도가 적당하고, 하나 이상은 제 3자가 작성하는 것이 좋다.
  • 많이 쓰일 클래스를 상속용으로 설계한다면 반드시 문서화한 내부 사용 패턴과 protected 메소드와 필드를 구현하면서 이 결정이 그 클래스의 성능과 기능에 계속 영향을 미친다는 것을 알아야 한다.
    • 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

AbstractList

상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메소드를 호출해서는 안 된다.

상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메소드가 하위 클래스의 생성자보다 먼저 호출된다. 이 때 재정의한 메소드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않는다.

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();
    }
}

이런식의 예시가 있다고 치면 instant를 두 번 출력할 거라는 기대가 있지만, 첫 번째는 상위 클래스의 생성자가 하위 클래스 생성자의 인스턴스 필드를 초기화 하기 전에 호출하기 때문에 null이 호출된다.

clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메소드를 호출해서는 안 된다.

  • Clonable이나 Serializable을 구현할지 정해야 한다면 이들을 구현할 때 따르는 제약도 생성자와 비슷하다는 점에 주의해야 한다.
  • readObject의 경우 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의한 메소드부터 호출하게 된다.
  • clone의 경우 하위 클래스의 clone 메소드가 복제본의 상태를 올바르게 수정하기 전에 재정의한 메소드를 호출한다.
  • clone이 잘못되어 깊은 복사를 하다가 원본 객체의 일부를 참조하고 있다면 원본 객체에까지도 피해를 줄 수 있다.

Serializable을 구현한 상속용 클래스가 readResolve나 write Replace 메소드를 갖는다면 이 메소드들은 private이 아닌 protected로 선언해야 한다.

  • private로 선언한다면 하위 클래스에서 무시된다.
  • 상속을 허용하기 위해 내부 구현을 클래스 API로 공개하는 예 중 하나다.
  • 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이 가장 좋다.

여태까지 상속용으로 클래스를 설계하려면 많은 노력이 들고 제약도 상당하다는 것을 알았다. 아래에서는 상속을 금지하는 방법에 대해서 정리하였다.

상속을 금지하는 방법

  • 클래스를 final로 선언한다.
  • 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어준다.

기존과 같이 구체 클래스를 상속해 계측, 통지, 동기화, 기능 제약 등의 일부 기능을 추가해야 하는 것이 아니라 상속을 금지하고 대신 아이템 18에서 설명한 래퍼 클래스 패턴을 사용하자.

만약 상속을 허용해야 하는 경우라면 재정의 가능 메소드를 호출하는 자기 사용 코드를 완벽하게 제거해야 한다.

클래스의 동작을 유지하면서 재정의 가능 메소드를 사용하는 코드를 제거하는 방법
  • 각각의 재정의 가능 메소드는 자신의 본문 코드를 private 도우미 메소드로 옮기고, 이 도우미 메소드를 호출하도록 수정한다.
  • 재정의 가능 메소드를 호출하는 다른 코드들도 모두 이 도우미 메소드를 직접 호출하도록 수정한다.