ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 클래스와 인터페이스 - 아이템 19. 상속을 고려해 설계하고 문서화하라, 그러지 않았다면 상속을 금지하라
    Study/Effective Java 2022. 12. 21. 10:25

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

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

    메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 한다. 달리 말하면, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. 더 넓게 말하자면, 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다.

    여기서 재정의 가능 메서드란, public과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.

     

    1. @imlSpec 태그

    메서드 주석에 붙여주면 자바독 도구가 "Implementation Requirements"로 시작하는, 메서드의 내부 동작 방식을 설명하는 절을 생성해 준다.

    이 태그를 활성화하기 위해서는 명령줄 매개변수로 -tag "implSpec:a:Implementation Requirements:"를 지정해주면 된다.

     

    2. protected 메서드와 필드

    이처럼, 내부 메커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아니다. 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다. 드물게는 protected 필드도 공개해야 할 수도 있다.

    그렇다면, 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까? 안타깝게도 마법은 없다. 심사숙고해서 잘 예측해본 다음, 실제 하위 클래스를 만들어 시험해보는 것이 최선이다.

    널리 쓰일 클래스를 상속용으로 설계한다면 여러분이 문서화한 내부 사용 패턴과 protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 잘 인식해야 한다. 이 결정들이 그 클래스의 성능과 기능에 영원한 족쇄가 될 수 있다. 그러니 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

     

    상속을 허용하는 클래스의 제약사항

    상속을 허용하는 클래스가 지켜야 할 제약이 몇 개 남았다.

    상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다. 이 규칙을 어기면 프로그램이 오동작할 것이다. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.

     

    아래 예시를 통해 구체적인 모습을 확인해 보겠다.

     

    public class Super {
    
        public Super() {   // 생성자가 재정의 가능 메서드를 호출한다.
            override();
        }
    
        public void override() {
        }
    }

    위 Super 클래스는 재정의 가능 메서드(override())를 호출하는 잘못된 코드이다.

     

    public class Sub extends Super {
    
        private final Instant instant;   // 초기화되지 않은 final 필드, 생성자에서 초기화된다.
    
        Sub() {
            instant = Instant.now();
        }
    
        @Override
        public void override() {   // 재정의 가능 메서드, 상위 클래스의 생성자가 호출한다.
            System.out.println(instant);
        }
    
        public static void main(String[] args) {
            Sub sub = new Sub();
            sub.override();
        }
    }

    위 Sub 클래스는 Super 클래스를 상속받는 하위 클래스의 코드로, override() 메서드를 재정의했다. 상위 클래스의 생성자가 호출해 오동작을 일으키는 코드이다.

     

    null
    2022-12-21T00:54:33.526744300Z

    프로그램을 실행했을 때, instant를 두 번 출력하리라 기대했겠지만, 첫 번째 instant는 null을 출력한다. 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe() 메서드를 호출하기 때문이다.

     

    Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다. 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다. clone()과 readObject() 메서드는 생성자와 비슷한 효과를 낸다.

    따라서, 상속용 클래스에서 Cloneable이나 Serializable 인터페이스를 구현할지 정해야 한다면, 이들을 구현할 때 따르는 제약도 생성자와 비슷하다는 점에 주의해야 한다. 즉, clone()과 readObject() 메서드 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.

     

    마지막으로, Serializable을 구현한 상속용 클래스가 readResolve()나 writeReplace() 메서드를 갖는다면, 이 메서드들은 private가 아닌 protected로 선언해야 한다. private로 선언한다면 하위 클래스에서 무시되기 때문이다.

     

    일반적인 구체 클래스 상속의 주의 사항

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

    상속을 금지하는 방법은 두 가지다. 둘 중 더 쉬운 쪽은 클래스를 final로 선언하는 방법이다. 두 번째 선택지는 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩토리를 만들어주는 방법이다.

     

    최종 정리

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


    출처

     이펙티브 자바 Effective Java 3/E. 조슈아 블로크 저자(글) · 개앞맵시(이복연) 번역

     

    728x90

    댓글

Designed by Tistory.