이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.
아이템 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
아이템 18에서 상속을 염두하지 않고 설계한 '외부' 클래스를 상속할 때에 대한 주의점을 알아보았다.
여기서 '외부'란, 프로그래머의 통제권 밖에 있어서 언제 어떻게 변경될지 모른다는 뜻.
상속을 고려한 설계와 문서화
* 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야 한다.
- 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다. 그런데 마침 호출되는 메서드가 재정의 가능 메서드인 경우 그 사실을 호출하는 메서드의 API 설명에 기입해야 한다.
- 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야한다.
** 재정의 가능 : public, protected 메서드 중 final 이 아닌 모든 메서드 - 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야한다.
- API문서의 메서드 설명 끝에서 종종 "Implementation Requiredments"로 시작하는 절을 볼 수 있다. 그 메서드 내부 동작 방식을 설명하는 곳이다. 이 절을 메서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다.
* 클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야할 수도 있다.
ex 1) java.util.AbstractList의 removeRange 메서드
removeRange 메서드는 단지 부분 리스트의 clear 메서드를 고성능으로 만들기 쉽게하기 위해서 protected 로 공개된 메서드다.
상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야할 까?
- 잘 예측해본 다음, 직접 하위 클래스를 만들어 확인, 시험해보자
(직접 하위클래스를 만들어서 상속 받아보고 전혀 쓰지 않는 멤버는 private으로 한다던가) - protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 한다.
(너무 적게 노출해서 상속으로 얻는 이점마다 없애지 않도록 주의하자)
* 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되고, 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않는다.
ex 2) 위의 규칙을 어기는 코드
public class Super {
// 잘못된 예 - 생성자가 재정의 가능한 메서드를 호출함.
public Super() {
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
// 초기화 되지 않은 final 필드 (생성자에서 초기화함)
private final Instant instant;
public 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를 두 번 출력할 것이라고 기대
- 상위클래스(Super)의 생성자에서 호출되는 overrideMe()
그 본클래스(Sub) overrideMe(); 로
- 상위클래스(Super)의 생성자에서 호출되는 overrideMe()
- 하지만, 첫 번째는 null 을 출력함.
- 상위 클래스(Super)의 생성자는 하위 클래스(Sub)의 생성자가 인스턴스 필드를 초기화하기 전에 overrideMe 를 호출하기 때문
- 참고) println은 null 입력도 받아들이기 때문에 NPE(NullPointerException)이 발생하지 않음
* Cloneable과 Serializable 인터페이스
cloneable과 Serializable 인터페이스를 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 좋지 않다.
- clone과 readObject 모두 직접적으로든 간적접으로든 재정의 가능 메서드를 호출해서는 안 된다.
clone과 readObject 메서드는 생성자와 비슷한 효과를 냄(새로운 객체를 생성) - readObject : 하위 클래스의 상태가 역질렬화되기 전에 재정의한 메서드부터 호출
- clone : 클래스의 clone 메서드가 복제본의 상태를 (올바른 상태로) 수정하기 전에 재정의한 메서드 호출
ex) 재정의한 메서드에서 원본 객체의 깊숙한 내부 자료구조까지 복제본으로 완벽히 복사됐다고 가정하고 복제본을 수정했다고 하자. 그런데 사실 clone이 완벽하게 구현되어있지 않아 복제본의 내부 어딘가에서 원본 객체를 바라보고 있다면 원본 객체에 영향이 갈 수 있다. - Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 한다.
(private으로 선언한다면 하위 클래스에서 무시되기 때문이며, 이 역시 상속을 허용하기 위해 내부 구현을 클래스 API로 공개하는 예 중 하나)
* 상속을 금지하는 법
1) 클래스를 final로 선언하는 방법
public final class example {
...
}
2) 모든 생성자를 private이나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
@Getter
public class Example {
private int idx;
private Example() {}
private Example(int idx) {
this.idx = idx;
}
public static Example getInstance() {
return new Example();
}
}
참고) 클래스의 동작을 유지하면서 재정의 가능 메서드를 사용하는 코드를 제거할 수 있는 기계적인 방법
각각의 재정의 가능 메서드는 자신의 본문 코드를 private '도우미 메서드'로 옮기고, 이 도우미 메서드를 호출하도록 수정한다. 그런 다음 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정하면 된다.
정리
- 상속용 클래스를 설계하기 위해서는 문서화를 잘 해야한다.
그렇지 않으면, 내부 구현 방식을 믿고 활용하던 하위 클래스에서 오동작이 발생할 수 있다. - 다른 이가 효율이 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야할 수도 있다.
- 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 수도 있다.
- 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록 만들면 된다.
'BE > Java' 카테고리의 다른 글
[Effective Java] 아이템 21. 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2022.09.28 |
---|---|
[Effective Java] 아이템 20. 추상 클래스보다는 인터페이스를 우선하라 (0) | 2022.09.27 |
[Effective Java] 아이템 18. 상속보다는 컴포지션을 사용하라 (1) | 2022.09.22 |
[Effective Java] 아이템 17. 변경 가능성을 최소화하라 (1) | 2022.09.20 |
[Effective Java] 아이템 16. public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2022.09.19 |