Home [Effective Java] Item19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라!
Post
Cancel

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

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

상속을 고려한 문서화

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
  • 호출되는 메서드가 재정의 가능 메서드일 경우, 그 사실을 API 설명에 적시한다.
    • 어떤 순서로 호출하는지 적시한다.
    • 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
  • “좋은 API 문서란 ‘어떻게’가 아닌 ‘무엇’을 하는지 설명해야 한다”는 격언과 대치됨.
    • 상속이 캡슐화를 해치기 때문에, 안전한 상속을 위해 어떻게를 기입해야 하기 때문이다.

Implementation Requirements

  • 메서드 내부 동작 방식을 설명하는 곳
  • 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.

상속을 고려한 설계

  • 클래스 내부 동작 과정 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드 형태로 공개해야 할 수 있다.
    • 효율적인 하위 클래스를 큰 어려움 없이 만들기 위해
  • 상속용 클래스 설계 시 어떤 메서드를 protected로 노출해야 할까?
    • 정답은 없다.
    • 심사숙고해서 잘 예측해보고, 실제 하위 클래스를 만들어 시험해보자.
    • 상속의 이점을 해치지 않는 선에서 가능한 한 protected로 공개된 메서드는 적게 구성한다. (내부 구현 공개에 해당하므로)
  • 널리 쓰일 클래스를 상속용으로 설계한다면, 문서화한 내부 사용 패턴 및 protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 한다.
    • 그러니 반드시 상속용 설계 클래스를 배포 전 하위 클래스로 검증해야 한다.
  • 클래스를 상속용으로 설계하려면 엄청난 노력이 들고, 제약도 상당하다.
    • 절대 가볍게 생각하고 상속용 클래스 설계에 임해서는 안된다.
    • 추상 클래스나 인터페이스의 골격 구현처럼 상속을 허용하는게 명백히 정당한 상황이 존재한다.
    • 불변 클래스처럼 상속용 설계를 하는 것이 명백히 잘못된 상황이 존재한다.

상속을 허용하는 클래스가 지켜야할 제약

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

  • 어기면 프로그램이 오작동한다.

    • 상위 클래스의 생성자는 하위 클래스의 생성자보다 먼저 호출된다.

    • 하위 클래스에서 재정의한 메서드가 상위 클래스 생성 시 하위 클래스 생성보다 먼저 호출된다.

    • 호출한 재정의 메서드가 하위 클래스 생성자에서 초기화하는 값에 의존하는 경우, 문제가 생긴다.

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      
      public class Super {
          public Super() {
              overrideMe(); // 재정의 가능한 메서드를 호출
          }
          public void overrideMe() {
          }
      }
      public final class Sub extends Super {
          private final Instacnce instacnce;
          Sub() {
              instacnce = Instacnce.now();
          }
        	// 재정의 가능한 메서드. 상위 클래스의 생성자가 호출하고 있음.
          @Override
          public void overrideMe() {
              System.out.println(instacnce);
          }
          public static void main(String[] args) {
              Sub sub = new Sub();
              sub.overrideMe();
          }
      }
      
      • instance를 두 번 출력하길 기대했으나, 처음은 null을 , 두 번째는 instance를 출력한다.
        • instance 생성 전 접근했으니 원래는 NullPointerException을 던질 것이다. 다만 println이 null 입력도 받기에 null을 출력했다.

Cloneable, Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다.

  • 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋은 생각이 아니다.
  • clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다.
    • 새로운 객체를 생성
    • readObject : 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의 메서드를 호출함.
    • clone : 하위 클래스의 clone 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의 메서드를 호출함.
      • 이는 복제본 뿐만 아니라 원본에도 영향을 미칠 수 있다.
  • 생성자와 비슷한 효과를 내기에 제약도 생성자와 비슷하다.
    • clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.

일반 구체 클래스의 경우

  • 전통적으로 final도 아니고, 상속용으로 설계되지도, 문서화 되지도 않았다.
    • 그대로 두면 하위 클래스 오작동을 유발할 위험이 존재한다.

문제 해결

  • 상속용으로 설계하지 않은 클래스는 상속을 금지한다.
    1. final로 설계한다.
    2. 모든 생성자를 private이나 package-private으로 선언하고, public 정적 팩터리를 만들어준다.
  • 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지한다면, 사용하기에 상당히 불편할 것이다.
    • 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 이 사실을 문서로 남긴다.

핵심 정리

  • 상속용 클래스 설계는 만만치 않으니 잘 고려하라.
  • 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남기자.
    • 문서화 한 내용은 그 클래스가 쓰이는 한 반드시 지켜야 한다.
    • 지키지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스의 오작동을 유발한다.
  • 효율 좋은 하위 클래스의 추가 생성을 위해 일부 메서드를 protected로 제공해야 할 수 있다.
    • 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하라.
    • 상속을 금지하려면 final로 선언하거나, 생성자의 외부 접근을 차단하라.
This post is licensed under younghwani by the author.

[Effective Java] Item18. 상속보다는 컴포지션을 사용하라!

[Effective Java] Item20. 추상 클래스보다는 인터페이스를 우선하라!

Comments powered by Disqus.