ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
    Book/Effective Java 3E 2022. 10. 26. 22:20
    반응형

      상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 합니다. '재정의 가능'이란 public과 protected 메서드 중 final이 아닌 모든 메서드를 뜻합니다.

     

      API 문서의 메서드 설명 끝에서 종종 "Implementation Requirements"로 시작하는 절을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳입니다. 이 절은 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해줍니다. @implSpec 태그는 자바 8에서 처음 도입되어 자바 9부터 본격적으로 사용되기 시작했습니다. 이 태그를 활성화하려면 명령줄 매개변수로 -tag "impleSpec:a:Implementation Requirements"를 지정해주면 됩니다.

     

      내부 메커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아닙니다. 효율적인 하위 클래스를 큰 어려움 없이 만들게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있습니다. 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 실제 하위 클래스를 만들어 시험해보는 것이 최선입니다.

     

      상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 됩니다. 아래 프로그램이 instant를 두 번 출력하리라 기대했겠지만, 첫 번째는 null을 출력합니다. 상위 클래스(Super)의 생성자는 하위 클래스(Sub)의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문입니다.

    package effectivejava.chapter4.item19;
    
    // 재정의 가능 메서드를 호출하는 생성자 - 따라 하지 말 것!
    public class Super {
        // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
        public Super() {
            overrideMe();
        }
    
        public void overrideMe() {
        }
    }
    package effectivejava.chapter4.item19;
    
    import java.time.Instant;
    
    // 생성자에서 호출하는 메서드를 재정의했을 때의 문제를 보여준다.
    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();
        }
    }

     

    private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.

     

      clone과 readObject 메서드는 생성자와 비슷한 효과를 냅니다(새로운 객체를 만든다). 따라서 상속용 클래스에서 Cloneable이나 Serializable을 구현한다면, clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 됩니다.

     

      구체 클래스는 전통적으로 final도 아니고 상속용으로 설계되거나 문서화되지도 않았습니다. 하지만 그대로 두면 위험하기에 상속용으로 설계하지 않은 클래스는 상속을 금지해야 합니다. 상속을 금지하는 방법은 두 가지입니다.

    1. 클래스를 final로 선언하는 방법
    2. 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법

      핵심 기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 개발하는 데 아무런 어려움이 없을 것입니다. Set, List, Map이 좋은 예입니다. Item 18에서 설명한 래퍼 클래스 패턴 역시 기능을 증강할 때 상속 대신 쓸 수 있는 더 나은 대안입니다.

     

    상속용 클래스를 설계하기란 결코 만만치 않다.

    클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며,

    일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.

    그러지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다.

    다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다.

    그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다.

    상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.