-
19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라JAVA/Effective java 2021. 3. 2. 12:20
- 상속을 고려한 설계와 문서화란 무얼 뜻할까?
- 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다.
- API 설명과 덧붙여서 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.
- 여기서 재정의 가능이란 pulbic과 protected 메서드 중 final이 아닌 모든 메서드를 뜻한다.
- Implementation Requirements로 시작하는 절을 API 문서의 메서드 설명에서 볼 수 있다.
- 이는 그 메서드의 내부 동작 방식을 설명하는 곳이다.
- 이 설명에 따르면 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줌을 확실히 알 수 있다.
- iterator 메서드로 얻은 반복자의 동작이 remove 메서드의 동작에 주는 영향도 정확히 설명했다.
- 하지만 이런 식은 '어떻게' 가 아닌 '무엇'을 하는지를 설명해야 하는 격언과 대치된다.
- 상속이 캡슐화를 해치기 때문에 일어나는 안타까운 현실이다.
- 클래스를 안전하게 상속할 수 있게 하기 위해 기술하지 않았어야 할 내부 구현 방식을 설명해야만 한다.
- 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
123456789101112131415161718192021222324252627282930/*** Removes from this list all of the elements whose index is between* {@code fromIndex}, inclusive, and {@code toIndex}, exclusive.* Shifts any succeeding elements to the left (reduces their index).* This call shortens the list by {@code (toIndex - fromIndex)} elements.* (If {@code toIndex==fromIndex}, this operation has no effect.)** <p>This method is called by the {@code clear} operation on this list* and its subLists. Overriding this method to take advantage of* the internals of the list implementation can <i>substantially</i>* improve the performance of the {@code clear} operation on this list* and its subLists.** @implSpec* This implementation gets a list iterator positioned before* {@code fromIndex}, and repeatedly calls {@code ListIterator.next}* followed by {@code ListIterator.remove} until the entire range has* been removed. <b>Note: if {@code ListIterator.remove} requires linear* time, this implementation requires quadratic time.</b>** @param fromIndex index of first element to be removed* @param toIndex index after last element to be removed*/protected void removeRange(int fromIndex, int toIndex) {ListIterator<E> it = listIterator(fromIndex);for (int i=0, n=toIndex-fromIndex; i<n; i++) {it.next();it.remove();}}cs - java.util.AbstractList의 removeRange 메서드를 살펴보자,
- List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다.
- 이 메서드를 그럼에도 불구하고 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다.
- 상속용 클래스를 설계할 때는 직접 하위 클래스를 만들어보는 것이 '유일'하다.
- 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈자리가 확연히 드러난다.
- 거꾸로, 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private이었어야 할 가능성이 크다.
- 널리 쓰일 클래스의 상속용으로 설계한다면 문서화한 내부 사용 패턴, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 잘 인식해야 한다.
- 이 결정들은 클래스의 성능과 기능에 족쇄가 될 수 있으니 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
또한 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것이다.
- 이 프로그램이 instant를 두번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다.
- 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문이다.
- overrideMe에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe를 호출할 때 NullPointerException을 던지게 된다.
- 이 프로그램이 NullPointerException을 던지지 않은 유일한 이유는 println이 null 입력도 받아들이기 때문이다.
- Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 한층 더해준다. 이 클래스를 확장하려는 프로그래머에게 부담을 지우기 때문이다.
- 물론 이 인터페이스들을 하위 클래스에서 구현하도록 하는 특별한 방법도 있다. clone과 readObject 메서드는 생성자와 비슷한 효과를 낸다.
- 즉 clone과 readObject 모두 직간접적으로 재정의 가능 메서드를 호출해서는 안된다.
readObject : 하위 클래스의 상태가 역직렬화되기 전에 재정의한 메서드부터 호출한다.
clone : 하위 클래스의 clone 메서드가 복제본의 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출한다.
- 어느 쪽이든 프로그램 오작동으로 이어질 수 있다.
- 일반적인 구체 클래스 역시 그대로 두면 위험하다. 이런 클래스는 final도 아니고 상속용으로 설계되거나 문서화되지도 않았다.
- 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들 수 있기 때문이다.
- 이 문제를 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.
- 이를 위한 첫번째 선택지로 클래스를 final로 선언할 수 있고 두 번째 선택지는 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법이다.
- 구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용하기에 상당히 불편해진다.
- 이런 클래스라도 상속을 허용해야 한다면 합당한 방법이 있다.
- 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남기는 것이다.
핵심정리
- 상속용 클래스를 설계하기란 결코 만만치 않다. 클래스 내부에서 스스로를 어떻게 사용하는지 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.
- 다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다.
- 그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 수 있다.
- 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두 외부에서 접근할 수 없도록 만들면 된다.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) 2021.03.04 20. 추상 클래스보다는 인터페이스를 우선하라 (0) 2021.03.03 상속보다는 컴포지션을 사용하라 (0) 2021.02.28 변경 가능성을 최소화하라 (0) 2021.02.11 public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) 2021.02.01