Effective Java - Item 19
Updated:
Effective Java
Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
상속용 클래스의 문서화
상속용 클래스는 API로 공개된 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는 문서로 남겨야한다.
클래스의 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있다. 그런데 마침 호출되는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야한다.
재정의 가능한 메서드를 어떤 순서로 호출하는지, 그 영향도 담아야한다.
재정의 가능 : public과 protected 메서드 중 final이 아닌 모든 메서드
/**
* {@inheritDoc}
*
* <p>Note that this implementation throws an
* <tt>UnsupportedOperationException</tt> if the iterator returned by this
* collection's iterator method does not implement the <tt>remove</tt>
* method and this collection contains the specified object.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
위 설명에 따르면 iterator 메서드를 재정의하면 remove 메서드 동작에 영향을 줌을 확실히 알 수 있다.
좋은 API 문서란 ‘어떻게’가 아닌 ‘무엇’을 하는지를 설명해야 한다.
상속을 위한 내부 설계
효율적인 하위 클래스를 만들기 위해 클래서의 내부 동작 과정 중간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.
java.util.AbstractList의 removeRange는 사용자 입장에서는 크게 신경쓰지 않는다.(아마 사용하지 않는다는 뜻, protected에 주목하라)
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();
}
}
하지만 이 메서드를 protected로 제공한 이유는 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 위해서다.
removeRange()의 문서 내용을 보면, clear()를 사용할 때 해당하는 리스트 구현의 내부 구조를 활용하도록 이 메서드를 재정의 하면 이 리스트와 부분리스트의 clear 연산 성능을 크게 개선할 수 있다고 되어있다.
public void clear() {
removeRange(0, size());
}
protected 메서드는 가능한 적어야 하지만, 너무 적게 노출하여 상속으로 얻는 이점마저 없애지 않도록 주의해야한다.
그렇다면 어떤 메서드를 protected로 선언해야할까? 이는 하위 클래스를 여러개 만들어 시험해보아야 한다.
하위 클래스를 여러개 만들 때 까지 사용하지 않는 protected 멤버는 private으로 변경한다.
상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야한다.
생성자 내부 재정의 가능 메서드 호출 금지
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로, 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다.
public class Super {
// 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다.
public Super() { // 1번
overrideMe();
}
public void overrideMe() {
}
}
public final class Sub extends Super {
private final Instant instant;
Sub() { // 3번
instant = Instant.now();
}
// 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다.
@Override
public void overrideMe() { // 2번 - 이 때의 instant는 초기화가 되지 않아 null이다
System.out.println(instant);
}
public static void main(String[] args) {
Sub sub = new Sub();
sub.overrideMe();
}
}
instant를 두 번출력되리라 생각하였지만, 첫 번째는 null을 출력한다.
하위 클래스의 생성자가 호출 되기도 전에, 상위 클래스의 overrideMe() 메서드를 먼저 호출하여 하위 클래스의 overrideMe가 호출되기 때문이다.
private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
Cloneable과 Serializable
둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각이다.
clone과 readObject 메서드는 생성자와 비슷한 효과를 내므로(새로운 객체 생성), 둘 다 재정의 가능한 메서드를 호출해서는 안된다.
clone 메서드는 복제를 생성할 때 원본 객체에 피해를 줄 수 있다.
상속용인 아닌 클래스는 상속을 금지하라
상속을 금지하는 방법은 final로 선언하거나, 생성자를 private / package-private으로 선언하고 public 정적 팩터리를 만들어 주는 것이다.
이외의 조언
- 구현 상속보다는 인터페이스 상속을 사용하는 것이 더 나은 대안이다.
- 상속을 허용하기 위해선 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남겨야한다.
- 재정의 가능 메서드는 자신의 코드를 private 도우미 메서드로 옮긴다.
public add(){
add();
}
private add(){ // 도우미 메서드
...
}
정리
상속용 클래스를 설계하기 위해 스스로 어떻게 사용하는지 문서로 남겨야한다.
다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있다.
그러니 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편이 나을 것이다.
상속을 금지하려면 클래스를 final로 선언하거나 생성자를 외부에서 접근할 수 없도록 한다.
출처
Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.