Item 19 - 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
상속을 고려한 설계와 문서화 📃
(1) 상속용 클래스는 내부 구현을 문서로 남겨야 한다.
- 상속용 클래스는 재정의 할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- 아래와 같이
@implSpec
태그를 이용해서 불필요하게 내부 구현 방식을 설명해야 한다.
(2) 클래스의 내부 동작 과정 중간에 끼어 들어갈 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
- protected 메서드 하나하나가 모두 내부 구현에 해당하므로 이는 캡슐화를 위반한다.
- 추가해야 할 protected 멤버를 놓칠 수도 있고, 불필요한 protected 멤버가 포함될 수도 있다.
- ex. AbstractList의
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 인터페이스가 상속용 설계의 어려움을 더한다.
clone
과readObject
모두 접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.clone
과readObject
메서드는 생성자와 비슷한 효과를 낸다. (새로운 객체 생성)
Serializable
을 구현한 상속용 클래스가readResolve
나writeReplace
메서드를 갖는다면 이 메서드들은 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()
호출 시SubClass
의helperMethod()
가 호출된다.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.