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

상속용 클래스의 문서화

  • 상속용 클래스는 overriding할 수 있는 method들을 내부적으로 어떻게 이용하는 지 문서로 남겨야 한다.
  • 클래스의 API로 공개된 method에서 클래스 자신의 또 다른 method를 호출할 수도 있는데, 이 method가 overriding이 가능하다면 method의 API 설명에 적어두어야 한다.
  • 어떤순서로 호출되는지, 호출 결과가 이어지는 처리에 어떤 영향을 줄 수 있는지도 문서화해야한다.

문서화 예시 : https://docs.oracle.com/javase/7/docs/api/java/util/AbstractCollection.html

API 문서의 method설명 끝에서 종종 ‘This Implementation’ ~ 로 시작하는 절을 볼 수 있는데 이 부분이 method의 내부 동작 방식을 설명하는 곳이다.

클래스를 안전하게 상속할 수 있도록 하려면, 내부 구현 방식에 대해 설명해주어야 한다.

( Java method 주석에 @implSpec tag를 붙여주면 자바독 도구가 생성해준다. )

상속용 클래스의 제약

  1. 상속용 클래스의 생성자는 overriding 가능한 method를 호출해서는 안된다.
    (private,final,static method는 overriding이 불가능하니 생성자에서 호출하여도 무관하다. )
1
2
3
4
5
6
7
8
public class Super {

public Super(){
overrideMe();
}
public void overrideMe(){ }
}

Super라고 하는 상위 class에서 생성자에 overriding가능한 method를 호출한 상황을 가정하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class Sub extends Super{

private final Instant instant;

Sub(){
instant = Instant.now();
}

public void overrideMe(){
System.out.println("instant = " + instant);
}


public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
// 실행결과
// instant = null
// instant = 2021-12-19T08:58:46.412929Z
}
}

Super class를 상속한 자식 class에서 부모 생성자를 호출 -> 부모 생성자에서 overrideMe() 호출 -> 자식class의 instant 필드는 초기화 되기전임으로 null 출력이 된다.

  1. clone과 readObject 모두 overriding 가능한 method를 호출해서는 안된다.

예를 들어 , clone과 readObject는 객체를 새로 만드는 데 중간에 overriding 한 method를 호출한 경우, 제대로 복사되지 않은 객체가 만들어질수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Parent implements Cloneable{

public Thing[] things;

@Override
protected Parent clone() {
try{
Parent copyedParent = (Parent) super.clone();
// 부모 class에서 가변필드를 복사하기전에 overriding가능한 method 호출
overridedMethod();
copyedParent.things = things.clone();
return copyedParent;
}
catch(CloneNotSupportedException e){
throw new AssertionError();
}
}

protected void overridedMethod(){}
}
1
2
3
4
5
6
public final class Child extends Parent {
@Override
protected void overridedMethod() {
throw new RuntimeException();
}
}
1
2
3
4
public static void main(String[] args) {
Parent child = new Child();
child.clone(); // RuntimeException
}

특히 clone이 잘못되면 복제본 뿐 아니라 원복객체에도 피해를 줄 수 있다, 가변필드는 복제본이랑 원본과 공유되고 있는 상태를 예시로 들 수 있을 것이다.

  1. Serializable 을 구현한 상속용 class가 readResolve 나 writeReplace method를 갖는다면 이 method들은 private 가 아닌 protected 로 선언해야 한다.
  • readResolve : 역직렬화 과정에서 호출되는 method
  • writeReplace : 직렬화된 상태에서 다시 객체로 만들떄 호출되는 method

private 로 선언한다면 하위 class에서 무시되기 떄문이다.

일반적인 구체 클래스

  • 상속용으로 만들지 않고, 문서화되지 않은 class는 개발할떄 필요에 의해서 상속해도 괜찮을까? 되도록이면 상속을 금지하고, 대신 핵심 기능들을 정의해놓은 인터페이스 구현을 하는 것을 권고하고 있다.

  • 상속을 금지하는 방법은 이전 item에서도 다루었듯이 class를 final로 선언하거나 생성자를 하위class에서 호출할수 없도록 막는 방법이 있다.

  • 꼭 상속을 해야 한다면, 상속할 class 내부에 overriding 가능한 method들을 호출하는 코드가 없도록 하고, 이를 문서화 해야한다.
1
2
3
4
5
6
7
8
public void doSomething(){
doAnotherThing();
}

public void doAnotherThing(){
// overriding 가능한 method를 호출하고 있음으로,
// 이를 private로 변경하고 문서화 해야한다.
}

overriding 가능한 자기 사용 코드를 완벽히 제거하면, method를 자식 class에서 overriding해도 다른 method와는 독립적으로 작동할수 있으므로, 안전하다.

정리

상속용 class를 설계하려면 내부동작 방식과 다른 method들을 어떻게 호출하는지 (자기 사용 패턴) 을 모두 문서화해야 하며, 한번 문서화하면 변경하면 안된다. 때문에 상속을 사용해야할 명확할 이유가 없다면 상속을 금지하는 것을 권고한다.

Comments