독후감/Effective JAVA

[Effective JAVA] 19 "상속을 고려해 설계하고 문서화하라"

G-egg 2022. 5. 10. 20:59
반응형

ITEM 19 "상속을 고려해 설계하고 문서화하라"

 

Item 18 에서 상속을 고려해두지 않고 설계한 클래스를 상속했을 때 문제점들을 살펴보았다.

이번 장에서 상속용 클래스를 설계하는 방법에 대해 자세히 살펴보자.

 

첫 번째, 메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨야 한다.

Item 18 에서 본 것처럼, 클래스 API 가 자신의 다른 메서드를 호출할 수 있다. 더 나아가 어떤 순서로 호출되는지, 각각의 호출 결과가 어떤 side-effect 를 일으키는지 호출했을 때 모든 상황을 문서로 남겨야 한다. 백그라운드 스레드나 정적 초기화 과정에도 호출되어 다른 영향을 끼칠 수 있으니 모두 작성하자.

 

API 문서의 메서드 설명 끝에 "Implementation Requirements" 로 시작되는 절을 볼 수 있는데, 이 부분이 바로 메서드의 내부 동작 방식을 설명하는 곳이다. @ImplSpec 태그를 메서드 주석에 붙여두면 자바독 도구가 자동으로 생성해준다.

 

java.util.AbstractCollection remove 메서드

 

java.util.AbstractCollection remove 메서드의 주석부분을 살펴보면 Iterator 메서드를 재정의하면 remove 메서드 동작에 영향을 미칠 수 있다는 것을 확실히 알 수 있다. "좋은 API 문서는 '어떻게' 가 아닌 '무엇을' 하는지 설명해야 한다" 라는 격언과 반대로 상속은 캡슐화를 해치기 때문에 이렇게 자세히 작성해두어야 한다.

 

두 번째, 클래스 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개하여야 한다.

하위 클래스에서 사용할 수 있도록 상위 클래스에서 미리 기능들을 제공할 필요가 있다.

 

java.util.AbstractList removeRange 메서드

 

java.util.AbstractList removeRange 메서드는 하위 클래스에서 부분 리스트의 clear 메서드를 고성능으로 제공하기 위해 만들어졌다고 한다. 하위 클래스에서 잘 사용할 수 있도록 메서드들을 미리 구현하여 protected 지시어로 제공하는데 어떤 메서드를 노출시킬지 결정하기가 쉽지 않다고 한다. 상속용 클래스의 하위 클래스들을 직접 만들어 보고 여러 개 만들었는데도 전혀 쓰이지 않는다면 private 이었어야 할 가능성이 크다. 이전 아이템에서 소개했듯이 상속용 클래스를 만들고 릴리즈하면 해당 클래스를 기반으로 하위 클래스들이 여러 개 생기므로, 반드시 하위 클래스를 만들어 검증해야 한다.

 

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

상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출될 수 있다. 하위 클래스 생성과 동시에 로직이 잘못 실행될 수 있으므로 주의가 필요하다.

 

// 절대 해서는 안 된다.
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();
    }
}

 

instant 가 두 번 출력된다고 기대하지만 첫 번째는 null 을 출력한다. 하위 클래스의 생성자가 해당 필드를 초기화하기도 전에 overrideMe 메서드가 불리기 때문이다.

 

네 번째, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.

Cloneable 과 Serializable 인터페이스를 구현한 클래스는 설계하기가 더 까다롭다. clone 메서드는 하위 클래스의 clone 메서드가 복제본의 상태를 수정하기 전에 재정의한 메서드를 호출할 수 있고, readObject 메서드는 하위 클래스의 상태가 미처 다 역직렬화하기 전에 재정의한 메서드를 호출할 수 있다. Serializable 인터페이스를 구현한 클래스가 readResolve 나 writeReplace 메서드를 갖는다면 이 메서드들은 private 가 아닌 protected 로 선언해야 한다.

 

가급적, 상속용으로 설계하지 않은 클래스는 상속을 금지하자.

클래스를 final 로 선언하거나 모든 생성자를 private 나 package-private 으로 선언하고, public 정적 팩토리 메서드를 만들어 두자. 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 준다.

 

만약 재정의 가능 메서드를 꼭 써야한다면, private 도우미 메서드로 옮기고 이 도우미 메서드를 호출하게끔 변경하자.

 

"상속용 클래스를 설계할 때 문서화를 잘 하자!

재정의 가능 메서드는 되도록이면 호출하지 말자"

반응형