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

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다. 


    핵심 정리

    • 상속용 클래스는 내부 구현을 문서로 남겨야 한다.
      • @implSpec을 사용할 수 있다.
      • 상속용 클래스의 재정의 가능한 메서드들에 @implSpec 태그를 이용해 문서화 한다.
    • 내부 동작 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드로 공개해야 한다.
    • 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다. 
    • 상속용 클래스의 생성자는 재정의 가능한 메서드를 호출해서는 안된다.
      • Cloneable(아이템 13)과 Serializable(아이템 86)을 구현할 때 조심해야 한다.
    • 상속용으로 설계한 클래스가 아니라면 상속을 금지한다.
      • final 클래스 또는 private, package-private 생성자. 

     


    상속용 클래스는 내부 구현을 문서로 남겨야 한다.

    상속용 클래스에서 재정의를 허용하는 메서드는 메서드의 내부 동작 원리를 문서화 해야한다. 어차피 캡슐화가 깨지기 때문에 해당 클래스를 상속받아 재정의하는 메서드에서는 상위 클래스의 내부 구현을 상세히 알아야 한다. 내부 구현을 알리는 문서화 용도로 사용할 수 있는 태그가 @implSpec이다. 

    @implSpec은 아래와 같이 주석 부분에서 태그로 사용할 수 있다. 그리고 javadoc 명령어를 이용해서 문서화한다면 특수한 명령어를 통해 javaDoc을 실행시켜야 한다. 

    public class ExtendableClass {
        /**
         * This method can be overriden to print any message.
         *
         * @ImplSpec
         * Please use System.out.println().
         */
        public void doSomething() {
            System.out.println("hello");
        }
    }

     

     

    내부 동작 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드로 공개 + 상속용으로 설계한 클래스는 배포 전 반드시 하위 클래스를 만들어 검증 

    위의 두 가지 팁은 하나로 묶어서 사용해야한다. 

    먼저 재정의를 허용할 메서드만 protected로 잘 선별해야한다는 것이다. 또한 선별된 결과가 이상적인지 확인하기 위해서 상속용으로 설계한 클래스의 하위 클래스를 여러 개 만들어서 검증한다. 이 때 검증 내역은 다음과 같다. 

    • 필요없는 메서드가 protected로 노출되지는 않았는지.
    • 필요한 메서드가 private로 남아있는 것은 아닌지 

     


    상속용 클래스의 생성자는 재정의 가능한 메서드를 호출해서는 안된다.

    상속용 클래스의 생성자는 하위 클래스에서 재정의 가능하도록 열려있는 메서드를 호출해서는 안된다. 만약 그렇게 호출할 경우 전혀 예상하지 못한 잘못된 동작이 발생할 수 있다. 아래 코드에서 예시를 확인할 수 있다.

    부모 클래스 생성자에서 재정의 가능한 메서드 호출 → 잘못된 동작 발생

    먼저 부모 클래스 Super를 살펴보면 다음과 같다.

    • Super 클래스는 재정의 가능한 메서드 overrideMe()를 가지고 있다.
    • Super 클래스의 생성자는 overrideMe()를 호출한다. 
    public class Super {
    
        public Super() {
            overrideMe();
        }
    
        void overrideMe() {
        }
    }

    그러면 Super 클래스를 상속한 Sub 클래스를 살펴보자.

    • Sub 클래스는 필드로 Instant를 가진다. 그런데 이 Instant는 Sub 클래스가 생성될 때 만들어진다. 
    • Sub 클래스가 생성될 때, super()를 통해 부모 클래스 Super가 생성된 다음에 Instant가 생성된다.
    public class Sub extends Super{
        // 초기화 되지 않은 final 필드. 생성자에서 초기화한다.
    
        private final Instant instant;
    
        Sub() {
            // super() // 자바 default
            this.instant = Instant.now();
        }
    
        // 재정의 가능한 메서드. 상위 클래스의 생성자가 호출된다.
        @Override
        void overrideMe() {
            System.out.println(instant);
        }
    
        public static void main(String[] args) {
            Sub sub = new Sub();
            sub.overrideMe();
        }
    }

    그리고 이 때 위의 main() 메서드를 실행하면 어떤 결과가 나올까?

    // 실행결과
    null
    2023-04-10T01:15:59.719913800Z

    첫번째 실행에 null이 마음에 걸린다. 이건 뭐가 문제일까? 다음과 같이 동작하면서 문제가 발생한다.

    1. Sub()를 생성
    2. Sub()는 super()를 통해 Super 클래스를 생성
    3. Super 클래스의 생성자는 자식 클래스 overrideMe()를 호출한다. 이 때, 자식 클래스의 Instant는 생성되기 전이므로 null이 된다. 

    다음과 같이 상속 가능한 메서드를 부모 클래스의 생성자에서 호출하는 경우, 위와 같이 이상하게 동작하는 문제가 발생할 수 있다. 따라서 이런 부분을 명확하게 인지하고 상속할 때 주의해서 코드를 작성해야한다. 

     

    Cloneable, Seraizliable 역시 조심해야 함. 

    Cloneable(아이템 13)과 Serializable(아이템 86)을 구현할 때 역시 조심해야 한다. 이 인터페이스들의 특징은 둘다 인스턴스를 만들어내는 인터페이스다.

    • Cloneable: 현재 객체와 동일한 객체를 만들어내는 인터페이스다.
    • Serializable:  직렬화 / 역질렬화 하면서 바이트 스트림을 객체로 복원하는 일이 벌어짐. 

    둘다 어떤 인스턴스를 만들어내는데, 생성자를 호출하는 것과 같은 역할을 한다. 그리고 Cloenable / Serializable에 각각 정의된 메서드들인 clone(), readObject()이 존재한다. 이 때, 구현하면서 이 메서드들을 재정의 해서 사용하게 된다. 따라서 이 부분도 위에서 이야기 한 '부모 클래스의 생성자에서 재정의 가능한 메서드를 호출하지 마라'와 동일한 맥락이 된다. 


    상속용으로 설계한 클래스가 아니라면 상속을 금지한다.

    이처럼 상속은 주의해야 할 점, 문서화 해야할 점이 굉장히 많다. 만약 상속을 사용한다면 주의해야 할 점을 잘 지키고, 문서화를 잘 해서 작성해야한다. 만약 상속을 사용하지 않는다면, 상속을 금지하고 컴포지션을 이용하도록 한다. 상속을 금지하는 방법은 다음과 같다.

    • final 클래스 → 상속이 불가능함.
    • private 생성자 → 해당 클래스 내부에서 상속 가능하다.
    • package-private 생성자 → 해당 패키지 내부에서 상속 가능하다

    일부 유연하게 상속을 조금이라도 가능하게 하려면 private 생성자 계열을 고려해보면 되고, 상속을 아예 금지할 것이라면 final 키워드를 클래스에 붙이면 된다. 

     


    정리

    • 상속에서 재정의 가능한 메서드들을 protected로 공개하고, 배포 전에 여러 자식 클래스를 만들어 검증한다.
    • 재정의 가능한 메서드들은 문서화를 잘 해주고, 내부 동작 원리를 잘 설명해야한다. 
    • 부모 클래스는 재정의 가능한 메서드들을 생성자에서 절대로 호출하면 안된다. 잘못된 동작이 발생할 수 있기 때문이다.
    • 상속을 고려하지 않는다면, 상속을 금지시키고 Composition을 이용한다. 

     

     

     

    댓글

    Designed by JB FACTORY