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

Jung Ho Seo·2020년 7월 16일
0

EffectiveJava

목록 보기
1/35
post-thumbnail

Inheritance

앞장에서 상속을 염두에 두지 않고 설계했고 상속할 때의 주의점도 문서화 해놓지 않은 '외부'클래스를 상속할 때의 위험을 경고했다. 여기서 '외부'란 프로그래머의 통제권 밖에 있어서 언제 어떻게 변경될지 모른다는 뜻이다.

상속을 고려한 설계와 문서화

우선, 메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 달리 말하면, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다.(전 장의 addAll 예시) 그런데 재정의가 가능한 메서드라면 그 사실을 api 명세에 적시해야 한다.

예시

// pacakge java.util.AbstractCollection

/**
     * {@inheritDoc}
     *
     * <p>This implementation iterates over the collection looking for the
     * specified element.  If it finds the element, it removes the element
     * from the collection using the iterator's remove method.
     *
     * <p>Note that this implementation throws an
     * <tt>UnsupportedOperationException</tt> if the iterator returned by this
     * collection's iterator method does not implement the <tt>remove</tt>
     * method and this collection contains the specified object.
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     */
     
public boolean remove(Object o);

이 설명에 따르면 iterator 메서드를 재정의 하면 remove 메서드의 동작에 영향을 줌을 확실히 알 수 있다. iterator메서드로 얻은 반복자의 동작이 remove 메서드의 동작에 주는 영향도 정확히 설명했다.

@implSpec 태그는 자바 8에서 처음 도입되어 자바9부터 본격적으로 사용됬다. 이 태그는 활성화 하는게 바람직하다고 생각하지만, 자바 11에서도 여전히 옵션으로 남아있다.

@implSpec

  • 자기사용 패턴(self-use pattern)에 대해서도 문서에 남겨 다른 프로그래머에게 그 메서드를 올바르게 재정의 하는 방법을 알려야 한다.
  • 일반적인 문서화 주석은 해당 메서드와 클라이언트 사이의 관계를 설명
    • @implSpec 주석은 해당 메서드와 하위 클래스 사이의 관계를 설명하여, 하위 클래스들이 그 메서드를 상속하거나 super 키워드를 이용해 호출할 때 그 메서드가 어떻게 동작하는지를 명확히 인지하고 사용하게 해야 한다.
  • -tag "implSpec:a:Implementation Requirement" 스위치를 키지 않으면 @implSpec 태그를 무시한다

출처 - https://jaehun2841.github.io/2019/02/24/effective-java-item56/#%EB%AC%B8%EC%84%9C%ED%99%94-%ED%83%9C%EA%B7%B8

hook, protected 메서드

18장의 addAll에서 호출되는 add메서드 역시 좋은 예시일것 같다

상속을 위해 효율적으로 하위 클래스를 어려움 없이 만들 수 있게 하려면, 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.

예시


// package java.util/AbstractList

 /**
     * Removes from this list all of the elements whose index is between
     * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.
     * Shifts any succeeding elements to the left (reduces their index).
     * This call shortens the list by {@code (toIndex - fromIndex)} elements.
     * (If {@code toIndex==fromIndex}, this operation has no effect.)
     *
     * <p>This method is called by the {@code clear} operation on this list
     * and its subLists.  Overriding this method to take advantage of
     * the internals of the list implementation can <i>substantially</i>
     * improve the performance of the {@code clear} operation on this list
     * and its subLists.
     *
     * <p>This implementation gets a list iterator positioned before
     * {@code fromIndex}, and repeatedly calls {@code ListIterator.next}
     * followed by {@code ListIterator.remove} until the entire range has
     * been removed.  <b>Note: if {@code ListIterator.remove} requires linear
     * time, this implementation requires quadratic time.</b>
     *
     * @param fromIndex index of first element to be removed
     * @param toIndex index after last element to be removed
     */
    protected void removeRange(int fromIndex, int toIndex) {
        ListIterator<E> it = listIterator(fromIndex);
        for (int i=0, n=toIndex-fromIndex; i<n; i++) {
            it.next();
            it.remove();
        }
    }

이 메서드를 제공하는 이유는 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.

상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지 어떻게 결정할까? 저자가 추천하는 최선의 방법은 실제로 하위 클래스를 하나 만들어 보는 것이다. 물론 protected는 하나하나가 내부의 구현이므로 그 수는 가능한 최소여야할것이다.

상속을 허용하는 클래스가 지켜야할 제약

아직 상속을 허용하는 클래스가 지켜야 할 제약이 몇가지 남아있다. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다 이 규칙을 어기면 프로그램이 오작동할 것이다.

상위 클래스의 생성자는 하위클래스의 생성자 보다 먼저 실행되고, 하위클래스에서 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면, 의도대로 동작하지 않을 것이다.

예시


// Super.java
public class Super {

    public Super() {
        // 잘못된 예시!!
        overrideMe();
    }

    public void overrideMe(){

    }
}


// Sub.java
public class Sub extends Super{

    private final Instant instant;

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

    @Override
    public void overrideMe() {
        System.out.println(instant);
    }

    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

질문! 이 Sub클래스를 실행했을때 결과는!?





이 예시가 instant를 두번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다. 상위 클래스의 생성자가 인스턴스 필드를 초기화 하기도 전에 먼저 override me를 호출하기 때문이다. final 필드의 상태가 이 프로그램에서 두가지다

Cloneable, Serializable

이 두개의 인터페이스는 상속용 설계에 한층 어려움을 더해준다. 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.

이 방법 들은 아이템 13, 86에서 설명한다

clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다. 즉, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.

정리

클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당함을 알았다. 절대 가볍게 생각하고 정할 문제가 아니다. 다음 장에서 볼 인터페이스의 골격 구현처럼 상속을 허용하는게 명백히 정당한 상황이 있고, 불변 클래스 처럼 명백히 잘못된 상황이 있다.

이러한 문제들을 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스의 상속 자체를 금지하는 것이다. (final 선언, private 생성자) 혹은 상속을 꼭 허용해야겠다고 한다면, 클래스 내부에서 재정으 ㅣ가능 메서드를 호출하는 자기 사용 코드를 모두 제거하는것이 좋다.

profile
책, 글, 개발

0개의 댓글