[이펙티브 자바] 아이템19 | 상속을 고려해 설계화하고 문서화하라 그러지 않았다면 상속을 금지해라

제롬·2022년 3월 6일
0

이펙티브자바

목록 보기
19/25

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

상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야한다. 간단히 말하면 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야한다.

[상속 문서화 주요내용]

  • 재정의 가능여부
  • 호출 순서
  • 호출 결과의 영향

API 문서의 메서드 설명 끝에서 종종 Implementation Requirements 로 시작하는 절을 볼 수 있는데, 해당 메서드의 내부 동작 방식을 설명하는 곳이다.

이 절은 메서드 주석에 @implSpec 태그를 붙이면 자바독 도구가 자동으로 생성해준다.

[java9 doc - AbstractCollection]

@implSpec 태그

  • 자바 8에서 처음 도입되어 자바9부터 사용되기 시작했다. 자바 11 자바독에서까지 선택사항으로 남아있다.
  • 이 태그를 활성화하려면 명령줄 매개변수로 -tag "implSpec:a:Implementation Requirements:"를 지정해주면 된다.

좋은 API문서란?

좋은 API문서란 [어떻게] 가 아닌 [무엇] 을 하는지를 설명해야한다. 클래스를 안전하게 상속할 수 있도록 하려면 (상속만 아니었다면 기술하지 않았어야 할) 내부구현 방식을 설명해야만 한다.

효율적인 하위 클래스를 만들려면?

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

[java9 doc - AbstractList]

protected로 노출해야하는 메서드 선택방법

  • 가능한 숫자가 적어야 한다. 다만, 너무 적게 노출해서 상속으로 얻는 이점을 없애는일이 없도록해야 한다.
  • 상속용 클래스를 시험하는 방법은 직접 하위클래스를 만드는 방법이 유일하다.
    • 하위 클래스 검증 개수로는 3개정도가 적정하다.
  • 꼭 필용한 protected 멤버를 놓쳤다면 하위 클래스 작성시 그 빈자리가 드러난다.
  • 하위 클래스를 여러개 만들 때까지 전혀 쓰이지않는 protected 멤버는 사실 private이어야 할 가능성이 크다.

상속용 설계 클래스는 검증이 반드시 필요하다.

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

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

  • 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스 생성자보다 먼저 실행된다.
  • 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는값에 의존한다면 의도대로 동작하지 않는다.
  • 상속용 클래스의 생성자는 직간접적으로 재정의 가능 메서드를 호출해서는 안된다.

[생성자가 재정의 가능 메서드를 호출한다.]

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

[override 메서드를 재 정의한 하위 클래스]

public class Sub extends Super{
    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();
    }
}

위 코드가 instant를 두번 출력할것이라 생각했겠지만 실제로는 nullinstant를 각각 한번씩 출력한다.

그 이유는 하위 클래스의 생성자가 인스턴스필드를 초기화하기도 전에 상위 클래스의 생성자가 overrideMe 메서드를 호출하기 때문이다.

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

clone과 readObject 메서드

  • clonereadObject는 생성자와 비슷한 효과를 낸다.
  • Cloneable이나 Serializable을 구현할지 정해야 한다면, 이들 구현시 따르는 제약도 생성자와 비슷하다는것을 인식해야 한다.
  • clonereadObject 모두 직간접적으로 재정의가능 메서드를 호출해서는 안된다.
  • readObject의 경우 하위 클래스의 상태가 미처다 역직렬화되기 전에 재정의한 메서드부터 호출하게 된다.
  • clone의 경우 하위 클래스의 clone메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.
    • clone이 잘못 복제되면 복제본뿐 아니라 원본객체에도 피해를 줄 수 있다.
  • Serializable을 구현한 상속용 클래스가 readResolvewriteReplace 메서드를 갖는다면 이 메서드들은 protected로 선언해야 한다.
    • private으로 선언되면 하위 클래스에서 무시되기 때문이다.

클래스 상속시 주의사항

  • 클래스를 상속용으로 설계하려면 엄청난 노력이 들고 그 클래스에 안기는 제약도 상당하다.
  • 추상 클래스나 인터페이스의 골격구현처럼 상속을 허용하는게 명백히 정당한 상황이 있고 불변 클래스처럼 명백히 잘못된 상황이 있다.
  • 일반적인 구체 클래스는 final도 아니고 상속용으로 설계되거나 문서화되지 않아 그대로 두면 위험하다. 클래스 변화시 오작동이 발생할 가능성이 있기 때문이다.

상속을 금지하는 두가지 방법

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

만약, 핵심기능을 정의한 인터페이스가 있고, 클래스가 그 인터페이스를 구현했다면 상속을 금지해도 괜찮다.

상속을 꼭 허용해야 한다면?

  • 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게한다.
  • 해당 사실을 문서로 남긴다.

이렇게 재정의 가능 메서드를 호출하는 자기 사용 코드를 완벽히 제거하면 메서드를 재정의해도 다른 메서드의 동작에 아무런 영향을 미치지 않게된다.

[정리]

상속용 클래스를 설계하려면 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.

만약, 이를 지키지 않으면 문서를 믿고 활용하던 하위 클래스를 오작동하게 만들 수 있다.

또한, 하위 클래스를 효율적으로 만들수 있도록 protected 접근제어자를 갖는 일부 메서드를 제공해야 할 수도 있다. 그러니 클래스를 확장해야 할 명확한 사유가 없다면 상속을 금지하는 편이 낫다.

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


[Reference]
이펙티브자바 아이템19
아이템19 - 상속을 고려해 설계화하고 문서화하라 그러지 않았다면 상속을 금지해라

0개의 댓글