[Effective Java] 아이템 19 : 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

Loopy·2022년 7월 8일
0

이펙티브 자바

목록 보기
18/76
post-thumbnail
post-custom-banner

🛠상속을 고려한 설계와 문서화란 정확히 무엇을 뜻하는지 알아보자.

☁️ 상속을 위한 좋은 설계란?

1. 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남거야 한다.

자바 8에 도입된 @implSpec 태그를 붙여주면 다음과 같이 자동으로 자바독 도구가 생성해준다.

public boolean remove(Object o)
...
**Implement Requirements**  _//메서드 내부 동작 방식 설명_
이 메서드는 컬렉션을 주어진 원소를 찾도록 구현되었다..

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

AbstractList 의 예시를 들어보자.

protected void removeRange(int fromIndex, int toIndex)
...
**Implementation Requirements**:
...
주의 : ListIterator.remove가 선형 시간이 걸리면 이 구현의 성능은 제곱에 비례한다.

상속용 클래스를 설계할 때, 어떤 메서드를 protected으로 노출해야 하는지는 어떻게 결정할까?

직접 하위 클래스를 만들어서, 시험하는 방법이 최선이다. 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스 작성 시 빈자리가 드러날 것이다. 대략 3개 정도의 하위 클래스 생성이 적당하다. 더욱 중요한 점은, 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다는 것이다.

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

상위 클래스의 생성자가 하위 클래스 생성자보다 먼저 실행되므로, 다형성으로 인해 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출되기 대문이다.

public class Super {
    // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출
    public Super() {
        overrideMe();
    }

    public void overrideMe() {
    }
}
public final 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();
        sub.overrideMe();
    }
}

상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe()를 호출하기 때문에 instant 가 아닌 null 값이 출력된다. 만약, instant 객체의 메서드를 호출하려 했다면 NullPointerException 이 발생했을 것이다.

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

두 메서드 모두 새로운 객체를 생성하기 때문에 생성자와 비슷한 효과를 낸다. 따라서, ClonableSerializabl 인터페이스 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 좋지 않다.

clone : 복제본의 상태를 올바르게 수정하기 전에 재정의한 메서드 호출
readObject : 하위 클래스가 역질렬화 되기 전에 재정의한 메서드부터 호출

5. Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 private가 아닌 protected로 선언해야 한다.

해당 메서드들은 내부 구현을 공개하지 않기 위해 기본적으로 private 으로 선언한다. 직렬화와 역직렬화 특성상 보안에 매우 취약하기 때문이다. 하지만, 상속을 하려 한다면 protected 으로 접근 제한을 한단계 낮춰야 하는 문제가 발생한다.

☁️ 총 정리

클래스를 상속용으로 설계하려면 엄청난 노력이 들고, 그 클래스에 안기는 제약도 상당하다.

  1. 추상 클래스나 인터페이스의 골격 구현 : 상속 사용
  2. 불변 클래스 : 상속 사용 X

그 외의 일반적인 구체 클래스는, 상속용으로 설계하지 않은 클래스는 상속을 금지해야 한다.

🔖 상속 금지의 두가지 방법
1. 클래스를 final 로 선언
2. 모든 생성자를 privatepackage-private 으로 선언하고, public 정적 팩터리를 생성

래퍼 클래스 패턴 역시 기능을 추가할 때 상속 대신 쓸 수 있는 대안인것처럼, Set, List, Map 과 같이 핵심 기능이 담긴 인터페이스를 구현하였다면 상속을 금지해도 좋다.

핵심 정리
상속용 클래스를 설계하기란 결코 만만치 않다. 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected으로 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 이유가 없으면 상속을 금지하는 것이 좋다. 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근 할 수 없도록 만들면 된다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!
post-custom-banner

0개의 댓글