JAVA/Effective Java

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

Garonguri 2022. 5. 3. 16:03
728x90

[ 상속 관련 문서화를 할 때의 규약 ]

 

- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.

- 호출되는 메서드가 재정의 할 수 있는 가능성이 있다면 해당 사실과 호출 순서, 각각의 호출 결과가 처리에 어떤 영향을 미치는지도 적어야 한다.

(재정의 가능 : public, protected method 중 non-final)

 

- 재정의 가능 메서드를 호출할 수 있는 모든 상황을 API 문서로 남겨야 한다.

(API 문서에서 내부 동작 방식을 결정하는 부분은 implementation Requirements 로 시작)

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    @java.io.Serial
    private static final long serialVersionUID = 362498820763181265L;

    /*
     * Implementation notes. ------------------> 이부분!
     *
     * This map usually acts as a binned (bucketed) hash table, but
     * when bins get too large, they are transformed into bins of
     * TreeNodes, each structured similarly to those in
     * java.util.TreeMap. Most methods try to use normal bins, but
     * relay to TreeNode methods when applicable (simply by checking
     * instanceof a node).  Bins of TreeNodes may be traversed and
     * used like any others, but additionally support faster lookup
     * when overpopulated. However, since the vast majority of bins in
     * normal use are not overpopulated, checking for existence of
     * tree bins may be delayed in the course of table methods.
 				...

- 클래스를 안전하게 상속할 수 있게 하려면 내부 구현 방식을 반드시 설명해야 한다. (@implSpec 주로 사용)


[ 상속용 고려한 클래스 설계 ]

 

- 효율적인 하위 클래스를 만들기 위하여 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected method 형태로(혹은 필드로) 공개해야 할 수도 있다.

- 상속용 클래스를 protected로 노출해야될지 말지 시험하는 방법은 실제 하위 클래스를 (3개 정도) 만들어 시험해 보는 것이 유일하다.

- 하위 클래스를 여러 개 만들 때까지 쓰이지 않는 protected 멤버는 private으로 만드는 것을 고려해봐라.

- 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증하자.

 


[ 상속을 허용하는 클래스가 지켜야 할 제약 ]

 

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

-> 프로그램이 오동작할 수도 있으니 주의

       -> 상위 클래스의 생성자는 하위 클래스의 생성자보다 먼저 실행되고, 따라서 하위 클래스에서 재정의한 메서드가 하위 클래스 생성자보다 먼저 호출되기 때문에 일어나는 오류.

       -> 만약 재정의한 메서드가 하위 클래스 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않아 오류가 생긴다.

 


[ Cloneable, Serializable ]

 

- Cloneable, Serializable Interface를 하나라도 구현한 클래스는 상속용으로 설계하지 않는 것이 좋다.

- clone과 readObject메서드는 새로운 객체를 만들며 생성자와 비슷한 효과를 낸다. 따라서 이들을 구현할 때 생성자 구현 제약을 고려해야 한다.

 

[ clone, readObject 메서드 사용 시 직/간접적으로 재정의 가능 메서드를 호출해서는 안되는 이유 ]

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

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

->  프로그램 오작동으로 이어질 수 밖에 없다. 특히 clone의 경우에는 복제본 뿐만 아니라 원본 객체에도 피해를 줄 수 있음.

 

- Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private가 아닌 proteted로 선언해야 한다. 

- private로 선언된다면 하위 클래스에서 무시된다.

 


[ 그 외 일반적인 구체 클래스 ]

- final도 아니고, 상속용으로 설계되거나 문서화되지도 않았기 때문에 잘못 상속하다간 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 할 수 있다.

- 어떻게 해결하냐 ? -> 상속을 금지한다. 

[ 상속을 금지하는 방법 ]

  • 1. 클래스를 final로 선언하는 방법.
  • 2. 모든 생성자를 private이나 package-private로 선언하고 public 정적 팩터리를 만들어주는 방법.
    • 내부에서 다양한 하위 클래스를 만들어 사용할 수 있어 유연하다.
  • 상속을 금지한다고 해 논란의 여지가 있을 수 있지만, 핵심 기능을 정의한 인터페이스가 있고 클래스가 그 인터페이스를 구현했다면,  상속을 금지한다고 해도 개발에는 어려움이 없다.
  • 상속 대신 래퍼 클래스를 사용하는 방법도 있다.

표준 인터페이스를 구현하지 않은 구체 클래스가 상속을 꼭 사용해야 한다면?

- 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 하고 문서화 해주는 것이 합당하다.

 


[ 클래스의 동작을 유지하며 재정의 가능 메서드를 사용하는 코드를 제거할 수 있는 방법 ]

  1. 각 제정의 가능 메서드는 자신의 본문 코드를 private 도우미 메서드(?)로 옮기고 이 도우미 메서드를 호출하도록 수정한다.
  2. 재정의 가능 메서드를 호출하는 다른 코드들도 모두 도우미 메서드를 직접 호출하도록 수정한다.
728x90