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

들어가며

상속용 클래스는 재정의할 수 있는 메소드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 클래스의 API로 공개된 메소드에서 클래스 자신의 또 다른 메소드를 호출할 수도 있다. 그런데 마침 호출되는 메소드가 재정의 가능 메소드라면 그 사실을 호출하는 메소드의 API 설명에 적시해야 한다. 추가로 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.

protected 한정자

내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아니다. 때로는 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 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
23
24
25
26
public class Super {
public Super() {
overrideMe();
}

public void overrideMe() {
}
}

public final class Sub extends Super {
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();
}
}

Cloneable과 Serializable

Cloneable과 Serializable 인터페이스를 확장한 상속용 클래스의 상속은 상속받을 클래스의 설계의 어려움을 한층 더해준다. 그리고 clone과 readObject 메소드는 생성자와 비슷한 효과를 낸다. ㄷ따라서 상속용 클래스에서 Cloneable과 Serializable을 구현할지 정해야 한다면 clone과 readObject 메소드도 생성자와 비슷한 제약인 직접적으로든 간접적으로든 재정의 가능 메서드를 호출하지 마라라는 제약에 걸린다. readObject는 하위 클래스의 상태가 미처 다 역직렬화 되기 전에 재정의한 메소드부터 호출한다. clone의 경우 하위 클래스의 clone 메소드가 복제본의 상태를 수정하기 전에 재정의한 메소드를 호출한다. 어느 쪽이든 오작동으로 이어진다. 또한 clone이 잘못되서 내부에 원본 객체의 데이터를 참조하고 있다면 원본 데이터 마저 훼손된다.

또한 Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메소드를 갖는다면 이 메소드들은 private가 아닌 protected로 선언해야 한다. 이 역시 상속을 허용하기 위해 내부 구현을 클래스 API로 공개하는 예 중 하나다.

비상속 클래스

상속용으로 설계하지 않은 클래스는 상속을 금지하면 된다. 두가지 방안이 있다. 첫째는 클래스를 final로 선언하는 방법이다. 둘째는 모든 생성자를 private나 package-private로 선언하고 public 정적 팩터리를 만들어 주는 방법이다.

핵심 기능을 정의한 인터페이스가 있고 구체 클래스가 이 인터페이스를 확장했다면 상속을 금지해도 아무런 어려움이 없다. 하지만 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용이 불편해진다. 이럴때는 클래스 내부에서 재정의 가능 메소드를 사용하지 않게 만들고 이 사실을 문서로 남기면 된다. 즉, 재정의 가능 메소드를 호출하는 자기 사용 코드를 완벽히 제거해야 한다.

클래스의 동작을 유지하면서 재정의 가능 메소드를 사용하는 코드를 제거할 수 있는 방법은 다음과 같다.

1
2
3
4
5
6
7
8
9
public class InheritanceClass {
public void someOverrideableMethod() {
someOverrideableHelperMethod();
}

private void someOverrideableHelperMethod() {
//do something...
}
}
Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/08/Languages/Effective%20JAVA/item19/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.