클래스의 상속을 허용하면 문제가 발생할 수 있다.
기존 클래스(A) 내부에서 먼저 정의 및 호출되는 함수(F)가 재정의 가능한 함수일 때, 다른 클래스(B)가 A 클래스를 상속한 다음 F 함수를 재정의(F’)하여 사용할 수 있는데 모종의 이유로 클래스 A 에서 F 함수를 수정하면 클래스 B 의 F’ 함수에서 문제가 생길 수 있다.
이러한 문제가 왜 발생하는지 좀 더 자세히 알아보고, 어떻게 방지할 수 있는지 알아보자.
주석을 잘 남겨라
• 외부에서 상속용 클래스의 메서드를 재정의하면 어떤 일이 일어나는지 정확히 정리하여 문서로 남겨라
• 상속용 클래스는 재정의 가능한 메서드들을 내부에서 어떻게 이용하는지 문서로 남겨야 한다.
• 클래스 내부에서 재정의 가능한 메서드를 호출하고 있다면, 그 사실과 어떤 순서로 호출하는지,
각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 문서로 남겨라
◦ 재정의 가능 메서드 : final이 아닌 모든 public, protected 메서드
• 아니 그냥 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨라!
◦ 백그라운드 스레드나 정적 초기화 과정에서도 호출이 일어날 수 있음
• 메서드에서 종종 Implementation Requirements로 시작하는 주석을 발견할 수 있는데, 이는 메서드의 내부 동작 방식에 대한 설명을
적어놓은 주석이다.
◦ Command Line에서 -tag “implSpec:a:Implementation Requirements:” 명령어를 입력해 @implSpec 어노테이션을 새롭게
지정할 수 있는데, 이 어노테이션을 주석에 붙이면 Implementation Requirements 라는 문구가 자동으로 생성된다.
(자바독의 커스텀 태그 기능)
◦ 자바 개발팀에서 내부적으로 사용하는 규약
◦ Annotation 이름을 반드시 @implSpec으로 해야할 필요는 없지만, 언젠가 표준 태그로 정의될지도 모르니
이왕이면 자바 개발팀과 같은 방식으로 사용하는 것을 추천한다. (by 옮긴이)
→ java.util.AbstractCollection의 remove 메서드
• @implSpec은 이 클래스를 상속하여 메서드를 재정의했을 때 나타날 효과를 상세히 설명하고 있다.
◦ java.util.AbstractCollection의 remove 메서드는 내부적으로 iterator의 remove 메서드를 사용하고 있다.
◦ 위 설명에 의하면 iterator 인터페이스를 구현한 구현체에 remove 메소드가 구현되어 있지 않은 경우
UnsupportedOperationException이 발생한다.
• iterator 메서드를 재정의하면 java.util.AbstractCollection의 remove 메서드의 동작에 영향을 준다.
◦ iterator 메서드를 재정의하면 반드시 remove 메소드도 함께 구현해 주어야 java.util.AbstractCollection의 remove 메서드를
오류 없이 사용할 수 있다.
• 이처럼 클래스를 안전하게 상속할 수 있도록 하려면 클래스의 내부 구현 방식을 설명으로 남겨야 한다.
◦ @implSpec 어노테이션을 적극 활용하라
클래스의 Hook 을 잘 선별하여 protected 메서드 형태로 공개하라
• 효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 내부 동작 과정에 끼어들 수 있는 훅(hook)을
protected 메서드(재정의 가능한)로 제공하는것이 좋다.
• removeRange 메서드는 특정 리스트 또는 부분 리스트의 clear 메서드에서 사용된다고 나와있다.
• 또한 해당 메서드를 재정의하면 특정 리스트 또는 부분 리스트의 clear 메서드를 고성능으로 만들 수 있다고 명시되어있다.
• removeRange 메서드를 protected 접근 제어자로 제공한 이유는 단지 하위 클래스의 clear 메서드를 고성능으로 만들기 쉽게
하기 위해서이다.
• 상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지는 어떻게 결정할까?
◦ 심사숙고해서 잘 예측해본 다음 실제 하위 클래스를 만들어 실험해보는것이 최선
- 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는것이 유일한 방법이다.
- 한 3개 정도 만들어 보는 것이 적당하다. (이 중 하나 이상은 제 3자가 작성해볼 것)
- 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.
• protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 하나 너무 적게 노출해서 상속으로 얻는 이점마저
없애지 않도록 주의해야 한다.
• 클래스를 확장해야 할 명확한 이유가 떠오르지 않는다면 상속을 금지하는 편이 낫다.
상속을 허용하는 클래스가 지켜야 할 제약
• 상속을 허용하는 클래스의 생성자는 클래스 내부의 재정의 가능 메서드를 호출하면 안된다.
◦ 프로그램 오작동의 원인이 됨
◦ 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출되기 때문
◦ 하위 클래스에서 재정의된 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
◦ 재정의된 메서드가 하위 클래스의 생성자에서 초기화되는 값에 의존한다면 의도대로 동작하지 않을 것
public class Super {
public Super() { overrideMe(); }
public void overrideMe() { ... }
}
public final class Sub extends Super {
private final Instant instant;
Sub() { instant = Instant.now(); }
@Override
public void overrideMe() { System.out.println(instant); }
}
Sub sub = new Sub(); // null 출력
sub.overrideMe(); // instant 출력
• 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화 하기도 전에 overrideMe 함수를 호출하기 때문에
이러한 문제가 발생한다.
• overrideMe 메서드에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe 메서드를 호출할 때
NullPointerException을 던지게 될 것이다.
◦ 위 코드에서는 println이 null 입력을 처리해주기 때문에 예외는 발생하지 않음
상속용 클래스의 설계와 Cloneable/Serializable 인터페이스
• 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.
◦ clone 과 readObject 메서드는 생성자와 비슷한 효과를 내기 때문 : 새로운 객체를 생성한다.
◦ 즉 상속용 클래스에서 Cloneable 또는 Serializable을 구현하게 된다면 이들을 구현할 때 따르는 제약도 생성자와 비슷하다.
◦ clone 과 readObject 메서드 모두 직/간접적으로 재정의 가능한 메서드를 호출해서는 안된다.
상속용으로 설계되지 않은 일반적인 구체 클래스
• 상속용으로 설계하지 않은 클래스는 상속을 금지하라
• 상속용으로 설계되지 않은 일반적인 구체 클래스는 final도 아니고 상속용으로 설계/문서화되지 않았다.
◦ 그대로 두면 위험하다. / 누가 상속해버릴지 몰라... 누가 줄줄이 도미노 문제 만들어놓을지 몰라...
• 클래스의 상속을 금지시키는 두가지 방법
◦ 클래스를 final로 선언하는 방법
◦ 모든 생성자를 private 또는 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법
- 클래스 내부에서 다양한 하위 클래스를 만들어 쓰는 등의 유연한 작업을 가능하게 해준다.
• 상속을 금지해도 괜찮은 것인가?
◦ 클래스가 특정 인터페이스를 구현한 클래스라면 상속을 금지해도 개발하는데 문제가 없다.
- 예) Set / List / Map
◦ 단 구체 클래스가 인터페이스를 구현하지 않았는데 상속을 금지해 버리면 상당히 불편해질 수 있다.
◦ 이런 경우에는 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 해당 사실을 문서로 남기자.
- 메서드를 재정의해도 다른 메서드의 동작에 영향을 주지 않게 만들기 위해
클래스 동작을 유지하면서, 재정의 가능 메서드를 사용한 부분을 제거하는 법
public class Super {
public Super() { overrideMe(); }
public void overrideMe() { ... }
}
public class Super {
public Super() { overrideMe(); }
public void overrideMe() { ... }
private void helperMethod() { ... }
}
public class Super {
public Super() { helperMethod(); }
public void overrideMe() { helperMethod(); }
private void helperMethod() { ... }
}
• 새로운 private helperMethod(도우미 메서드)를 생성한다.
• 각각의 재정의 가능 메서드에 작성된 코드를 helperMethod로 옮긴다.
• 기존 재정의 가능 메서드가 이 helperMethod를 호출하도록 수정한다.
• 기존 재정의 가능 메서드를 호출하는 다른 코드 역시 이 helperMethod를 호출하도록 수정한다.
'Java' 카테고리의 다른 글
[Effective Java] ITEM29 : 이왕이면 제네릭 타입으로 만들라 (0) | 2022.07.31 |
---|---|
[Effective Java] ITEM24 : 멤버 클래스는 되도록 static으로 만들라 (0) | 2022.07.31 |
[Effective Java] ITEM14 : Comparable을 구현할지 고려하라 (0) | 2022.07.30 |
[Effective Java] ITEM9 : try-finally 보다는 try-with-resources를 사용하라 (0) | 2022.07.24 |
[Effective Java] ITEM7 : 다 쓴 객체 참조를 해제하라 (0) | 2022.07.24 |