Post

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

상속을 고려한 설계와 문서화 📃

(1) 상속용 클래스는 내부 구현을 문서로 남겨야 한다.

  • 상속용 클래스는 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
  • 아래와 같이 @implSpec 태그를 이용해서 불필요하게 내부 구현 방식을 설명해야 한다.

implSpec

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

  • protected 메서드 하나하나가 모두 내부 구현에 해당하므로 이는 캡슐화를 위반한다.
  • 추가해야 할 protected 멤버를 놓칠 수도 있고, 불필요한 protected 멤버가 포함될 수도 있다.
  • ex. AbstractList의 removeRange 메서드는 단지 clear 메서드의 성능 향상을 위해 사용된다.

removeRange

clear

(3) 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

  • 문서화 한 내부 사용 패턴과 protected 메서드와 필드는 영구적으로 사용될 수 있다.
  • 따라서 직접 하위 클래스를 만들어 정상적으로 작동하는지 검증하자!

(4) 상속을 허용하는 클래스가 지켜야 할 제약이 추가로 존재한다.

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

1
2
3
4
5
6
7
8
9
public class Super {
    // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다
    public Super() {
        overrideMe();
    }

    public void overrideMe() {
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public 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(); // null
        sub.overrideMe(); // 2024-08-31T22:14:22.066884Z
    }
}
  • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행된다.
  • 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
  • 이때, 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값을 의존한다면 의도대로 동작하지 않는다.
    • JVM 내부 구현에 의존하고 있다. (아쉬운 점)

(5) Cloneable과 Serializable 인터페이스가 상속용 설계의 어려움을 더한다.

  • clonereadObject 모두 접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
    • clonereadObject 메서드는 생성자와 비슷한 효과를 낸다. (새로운 객체 생성)
  • Serializable을 구현한 상속용 클래스가 readResolvewriteReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야한다. (private은 하위 클래스에서 무시되기 때문)

상속을 금지하는 방법 ❌

(1) 상속용으로 설계하지 않은 클래스는 상속을 금지하라.

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

(2) 상속을 금지하고 래퍼 클래스 패턴을 사용하는 것이 좋다.

(3) 상속을 사용해야 한다면, 재정의 가능 메서드를 호출하는 자기 사용 코드를 제거하라.

예시로 이해해보자.

👎 기존 코드 (재정의 가능 메서드가 직접 구현된 상태)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SuperClass {
    // 재정의 가능한 메서드
    public void publicMethod() {
        System.out.println("SuperClass: publicMethod");
        helperMethod();
    }

    // 재정의 가능한 메서드
    protected void helperMethod() {
        System.out.println("SuperClass: helperMethod");
    }
}

public class SubClass extends SuperClass {
    // 메서드를 재정의
    @Override
    protected void helperMethod() {
        System.out.println("SubClass: helperMethod");
    }
}
  • SubClass에서 helperMethod()를 재정의 할 수 있으며, publicMethod() 호출 시 SubClasshelperMethod()가 호출된다.
  • SuperClass에서 의도했던 동작이 깨질 수 있다.

👍️ 개선된 코드 (private 도우미 메서드로 분리)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SuperClass {
    // 재정의 가능 메서드
    public void publicMethod() {
        System.out.println("SuperClass: publicMethod");
        privateHelperMethod();  // private 메서드 호출로 변경
    }

    // 재정의 가능 메서드를 private 도우미 메서드로 분리
    private void privateHelperMethod() {
        System.out.println("SuperClass: privateHelperMethod");
    }
}

public class SubClass extends SuperClass {
    // 재정의 불가능한 메서드를 만들었기 때문에 더 이상 영향 없음
}
  • helperMethod()의 구현이 SuperClass에서 더 이상 재정의 되지 않도록 private 도우미 메서드privateHelperMethod()로 옮겼다.
  • publicMethod()helperMethod()가 아닌 privateHelperMethod()를 호출하도록 수정했다.
  • SubClass에서 더 이상 helperMethod()를 재정의 할 수 없으므로 SuperClass의 동작을 변경할 수 없다.

💡 핵심 정리

  • 상속용 클래스를 설계하기 위해선 자기 사용 패턴을 모두 문서로 남겨야 하고 지켜야 한다.
  • 그렇지 않으면, 하위 클래스에서 오동작이 발생한다.
  • 클래스를 확장해야 할 명확한 이유가 없다면 상속을 금지해라.
    • 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.
This post is licensed under CC BY 4.0 by the author.