설계 원칙 1. 상속용 클래스는 내부 구현을 문서로 남겨야 한다
"좋은 API 문서란 '어떻게'가 아닌 '무엇'을 하는지를 설명해야 한다"라는 격언과 달리 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 합니다.
이는 상속을 하는 순간 캡슐화가 깨지기 때문입니다.
만약 상위 클래스 메서드의 내부 구현을 모르는 상태로 개발을 진행할 경우 아이템 18에서 다루었던 문제가 또다시 발생할 것입니다.
- 복습하자면 HashSet을 상속한 InstrumentedHashSet 클래스가 원소가 추가될 때마다 계측하는 용도로 add()와 addAll() 메서드를 재정의
- 재정의한 addAll() 메서드가 내부적으로 count 값을 올리고 원소 추가를 위해 상위 클래스의 addAll() 호출
- 재정의한 add() 메서드 또한 내부적으로 count 값을 올리고 원소 추가를 위해 상위 클래스의 add() 호출
- 상위 클래스의 addAll() 메서드는 내부적으로 add 메서드를 호출하는데 하위 클래스에서 재정의한 add를 호출하여 더블 카운팅 문제 발생
이로 인해 상위 클래스를 상속한 하위 클래스에서 메서드를 재정의할 때, 상위 클래스의 메서드 구현 내용을 이해해야 하며 Java 8 버전 이상에서는 @implSpec 태그를 사용하여 주석으로 구현 내용을 작성하면 JavaDoc에 표시되는 기능이 추가되었습니다.
- @implSpec은 어노테이션이 아닌 단순 태그
@implSpec 사용 방법은 아래와 같습니다.
- 재정의 가능하도록 설게된 메서드에 주석으로 @implSpec 태그와 함께 내부 구현 내용 작성
- 터미널에 javadoc -d target/apidoc [패키지 경로] -tag "implSpec:a:[제목]" 입력
- ex) javadoc -d target/apidoc src/main/java/com/tistory/jaimemin/effectivejava/ch03/item19/implspec/* -tag "implSpec:a:Implementation Requirements:"
설계 원칙 2. 클래스의 내부 동작 과정 중간에 끼어들 수 있는 hook을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
java.util.AbstractList 클래스의 removeRange 메서드에 대한 java Doc은 아래와 같습니다.
List 구현체의 사용자는 removeRange 메서드에 관심이 없겠지만 그럼에도 불구하고 해당 메서드를 제공한 이유는 하위 클래스에서 부분 리스트의 clear 메서드를 구현하기 쉽게 지원하기 위해서입니다.
- 해당 메서드가 없었다면 하위 클래스에서 clear 메서드 시간복잡도가 O(N^2)이 되어 성능이 저하되고 부분 리스트를 밑바닥부터 구현해야 했을 것
위 예시처럼 상속용 클래스를 설계할 때 특정 메서드를 protected로 노출시켜 다른 메서드와 연결되도록 하면 성능도 챙기고 구현도 편해질 수 있습니다.
그러나 유감스럽게도 어떤 메서드를 protected로 노출해야 하는 기준은 명확하지 않습니다.
따라서 클래스 설계자가 신중하게 판단한 뒤 특정 메서드들을 protected로 노출하고, 직접 하위 클래스를 생성하여 테스트하는 것이 가장 좋으며 저자의 경험상 하위 클래스 3개를 만들어 테스트하는 것이 적당하다고 합니다.
테스트할 때 아래 항목들을 확인해야 합니다.
- protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 최소화해야하지만 너무 적을 경우 상속으로 얻는 이점이 없을 수 있음
- 꼭 필요한 protected 멤버가 있는데 누락되지 않았는지 확인 필요
- 하위 클래스를 3개 만들 때까지 전혀 쓰이지 않는 protected 멤버 혹은 메서드가 있을 경우 이는 사실상 private이어야 할 가능성이 큼
설계 원칙 3. 상속용 클래스의 생성자는 직간접적으로 재정의 가능 메서드를 호출해서는 안 된다.
상위 클래스 내 하위 클래스에서 재정의한 메서드를 호출하는 경우 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되기 때문에 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다.
위 내용은 아래 코드를 통해 확인이 가능합니다.
흐름
- 하위 클래스인 Sub보다 상위 클래스인 Super 생성자가 먼저 호출되어 하위 클래스에서 재정의된 overrideme 호출
- 이때 instant는 아직 초기화되지 않았기 때문에 null을 출력
- 이후 하위 생성자가 호출되어 instant 초기화
- overrideMe 메서드가 호출되어 초기화된 instant 값 출력
위 코드는 만약 재정의한 overrideMe 메서드가 null을 허용하지 않을 경우 NPE가 발생했을 수 있는 위험한 코드입니다.
3.1 Cloneable, Serializable
Clonable 혹은 Serializable 인터페이스를 구현한 클래스를 상속하는 것은 권장하지 않습니다.
아이템 13에서 다루었던 것처럼 상속용 클래스에서 Cloneable을 구현할 경우 하위 클래스를 작성하는 개발자에게 clone 메서드 구약을 모두 따르며 구현하는 것을 강요하는 꼴이기 때문입니다.
- clone과 readObject 메서드가 생성자와 비슷한 효과를 내기 때문에 이들을 구현할 때 앞서 설명한 제약사항을 고려 필요
- clone과 readObject 모두 직간접적으로 재정의 가능 메서드를 호출해서는 안됨
정리
상속에는 많은 제약사항이 있어서 지켜야 할 규약이 많습니다.
따라서 상속을 목적으로 설계되지 않은 클래스의 경우, 미래의 부작용을 방지하기 위해 상속을 제한하는 것이 권장됩니다.
상속을 제한하는 방법은 아래와 같이 두 가지이며 보다 자세한 내용은 아이템 17을 참고하시면 됩니다.
- 클래스 자체를 final로 선언
- 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩토리 메서드 제공
참고
이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 21] 인터페이스는 구현하는 쪽을 생각해 설계하라 (1) | 2024.02.07 |
---|---|
[아이템 20] 추상 클래스보다는 인터페이스를 우선하라 (0) | 2024.02.07 |
[아이템 18] 상속보다는 컴포지션을 사용하라 (1) | 2024.02.05 |
[아이템 17] 변경 가능성을 최소화하라 (0) | 2024.02.05 |
[아이템 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (1) | 2024.02.01 |