Search
🙉

이펙티브 자바:: 아이템 19 <상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라>

Intro::

이펙티브 자바 정리본입니다.

결론

상속용 클래스를 설계하는 것은 상당히 어렵다.
클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 합니다. 그렇지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있습니다.
효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있습니다.
클래스를 확장해야 하는 명확한 이유가 없다면 상속을 금지하는 것이 낫습니다.
상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록(정적 팩토리 메서드 참고) 만들면 됩니다.

왜 문서화 해야하는가?

내부 동작 방식을 모르고 상속받아 사용하게 된다면, 코드를 일절 수정하지 않았지만 상위 클래스가 변경된다면 의도하지 않은 동작이 발생할 수 있습니다.
하지만 이런 식은 “좋은 API 문서란 ‘어떻게’가 아닌 ‘무엇’을 하는지를 설명해야 한다”라는 격언과는 대치됩니다. 상속이 캡슐화를 해치기 때문에 클래스를 안전하게 상속할 수 있도록 하려면 어쩔 수 없이 내부 구현 방식을 설명해야만 합니다.

상속용 클래스를 설계할때 어떤 메서드를 protected로 노출해야 하는가?

상속용 클래스를 시험하며 직접 하위 클래스를 만들어보며 예측하는 방법밖에 없다.
널리 쓰일 클래스를 상속용으로 설계한다면 문서화한 내부 사용 패턴과, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 잘 인식해야합니다. 때문에 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 합니다.

주의할 점

1.
상속용 클래스의 생성자는 직접적이든 간접적으로든 재정의 가능 메서드를 호출해서는 안됩니다.
a.
상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다. 이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것 입니다.
public class Super { // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출한다. public Super() { overrideMe(); } public void overrideMe() { System.out.println("Super override me"); } } public final class Sub extends Super { // 초기화되지 않은 final 필드. 생성자에서 초기화한다. private final Instant instant; Sub() { instant = Instant.now(); } // 재정의 가능 메서드. 상위 클래스의 생성자가 호출한다. @Override public void overrideMe() { System.out.println(instant); } public static void main(String[] args) { Sub sub = new Sub(); sub.overrideMe(); } }
Java
복사
null 2024-08-22T08:14:53.114349500Z
Java
복사
Super 생성자에서 overrideMe();를 호출하게되면 재정의된 Sub.overrideMe()가 호출되는데 아직 instant가 초기화 되지 않은 상태이기 때문에 null 이 결과로 출력된다.
2.
Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 더해주기 때문에 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각입니다.
a.
Clone과 readObject 모두 직접적으로든 간접적으로든 재정의 기능 메서드를 호출해서는 안됩니다.
b.
Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 합니다.
i.
private으로 선언한다면 하위 클래스에서 무시되기 때문입니다.
c.

질문

List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없음에도 이 메서드를 제공하는 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서 라는 말이 무슨 말인가?

// ArrayList public void clear() { modCount++; final Object[] es = elementData; for (int to = size, i = size = 0; i < to; i++) es[i] = null; }
Java
복사
// AbstractList public void clear() { removeRange(0, size()); } 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(); } }
Java
복사
AbstractList에서 구현한 removeRange의 경우 protected로 가장 기본적인 삭제 동작을 하기 때문에 하위 클래스에서 성능적 이점을 위해 메서드 재정의를 할 수 있다고 생각하면 된다.

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)