ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
    JAVA/Effective java 2021. 3. 2. 12:20
    • 상속을 고려한 설계와 문서화란 무얼 뜻할까?

     

    • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
    • API 설명과 덧붙여서 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
    • 여기서 재정의 가능이란 pulbic과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.

     

     

    java.util.AbstractCollectiondml remove 메서드

    • Implementation Requirements로 시작하는 절을 API 문서의 메서드 설명에서 볼 수 있다.
    • 이는 그 메서드의 내부 동작 방식을 설명하는 곳이다. 
    • 이 설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줌을 확실히 알 수 있다.
    • iterator 메서드로 얻은 반복자의 동작이 remove 메서드의 동작에 주는 영향도 정확히 설명했다.

     

    • 하지만 이런 식은 '어떻게' 가 아닌 '무엇'을 하는지를 설명해야 하는 격언과 대치된다.
    • 상속이 캡슐화를 해치기 때문에 일어나는 안타까운 현실이다.
    • 클래스를 안전하게 상속할 수 있게 하기 위해 기술하지 않았어야 할 내부 구현 방식을 설명해야만 한다.

     


     

    • 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 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
    27
    28
    29
    30
    /**
         * 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.
         *
         * @implSpec
         * 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();
            }
        }
    cs

     

    • java.util.AbstractList의 removeRange 메서드를 살펴보자,
    • List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다.
    • 이 메서드를 그럼에도 불구하고 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.

     


    • 상속용 클래스를 설계할 때는 직접 하위 클래스를 만들어보는 것이 '유일'하다.

     

    • 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연히 드러난다.
    • 거꾸로, 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private이었어야 할 가능성이 크다.
    • 널리 쓰일 클래스의 상속용으로 설계한다면 문서화한 내부 사용 패턴, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 잘 인식해야 한다.
    • 이 결정들은 클래스의 성능과 기능에 족쇄가 될 수 있으니 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

     

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

    상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.

    이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.

     

     

     

    • 이 프로그램이 instant를 두번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다.
    • 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
    • overrideMe에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe를 호출할 때 NullPointerException을 던지게 된다.
    • 이 프로그램이 NullPointerException을 던지지 않은 유일한 이유는 println이 null 입력도 받아들이기 때문이다.

     

     


     

    • Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다. 이 클래스를 확장하려는 프로그래머에게 부담을 지우기 때문이다.
    • 물론 이 인터페이스들을 하위 클래스에서 구현하도록 하는 특별한 방법도 있다. clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다.
    • 즉 clone과 readObject 모두 직간접적으로 재정의 가능 메서드를 호출해서는 안된다.

     

    readObject : 하위 클래스의 상태가 역직렬화되기 전에 재정의한 메서드부터 호출한다.

    clone : 하위 클래스의 clone 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.

     

    • 어느 쪽이든 프로그램 오작동으로 이어질 수 있다.

     

     


     

    • 일반적인 구체 클래스 역시 그대로 두면 위험하다. 이런 클래스는 final도 아니고 상속용으로 설계되거나 문서화되지도 않았다.
    • 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들 수 있기 때문이다.
    • 이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.

     

    • 이를 위한 첫번째 선택지로 클래스를 final로 선언할 수 있고 두 번째 선택지는 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법이다.

     

     

    • 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용하기에 상당히 불편해진다.
    • 이런 클래스라도 상속을 허용해야 한다면 합당한 방법이 있다.
    • 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남기는 것이다.

     

     

     

    핵심정리


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

     

     

     

    참고 자료 


    이펙티브 자바

Designed by Tistory.