Intro::
이펙티브 자바 정리본입니다.
결론
- 상속용 클래스를 설계하는 것은 상당히 어렵다.
- 클래스 내부에서 스스로를 어떻게 사용하는지(자기사용 패턴) 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 합니다. 그렇지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있습니다.
- 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야 할 수도 있습니다.
- 클래스를 확장해야 하는 명확한 이유가 없다면 상속을 금지하는 것이 낫습니다.
- 상속을 금지하려면 클래스를 final로 선언하거나 생성자 모두를 외부에서 접근할 수 없도록(정적 팩토리 메서드 참고) 만들면 됩니다.
왜 문서화 해야하는가?
- 내부 동작 방식을 모르고 상속받아 사용하게 된다면, 코드를 일절 수정하지 않았지만 상위 클래스가 변경된다면 의도하지 않은 동작이 발생할 수 있습니다.
하지만 이런 식은 “좋은 API 문서란 ‘어떻게’가 아닌 ‘무엇’을 하는지를 설명해야 한다”라는 격언과는 대치됩니다. 상속이 캡슐화를 해치기 때문에 클래스를 안전하게 상속할 수 있도록 하려면 어쩔 수 없이 내부 구현 방식을 설명해야만 합니다.
상속용 클래스를 설계할때 어떤 메서드를 protected로 노출해야 하는가?
- 상속용 클래스를 시험하며 직접 하위 클래스를 만들어보며 예측하는 방법밖에 없다.
널리 쓰일 클래스를 상속용으로 설계한다면 문서화한 내부 사용 패턴과, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야 함을 잘 인식해야합니다. 때문에 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 합니다.
주의할 점
- 상속용 클래스의 생성자는 직접적이든 간접적으로든 재정의 가능 메서드를 호출해서는 안됩니다.
- 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 실행되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출됩니다. 이때 그 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 동작하지 않을 것 입니다.
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(); } }
null 2024-08-22T08:14:53.114349500Z
Super 생성자에서 overrideMe();를 호출하게되면 재정의된 Sub.overrideMe()가 호출되는데 아직 instant가 초기화 되지 않은 상태이기 때문에 null 이 결과로 출력된다.
- Cloneable과 Serializable 인터페이스는 상속용 설계의 어려움을 더해주기 때문에 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각입니다.
- Clone과 readObject 모두 직접적으로든 간접적으로든 재정의 기능 메서드를 호출해서는 안됩니다.
- Serializable을 구현한 상속용 클래스가 readResolve나 writeReplace 메서드를 갖는다면 이 메서드들은 private이 아닌 protected로 선언해야 합니다.
- private으로 선언한다면 하위 클래스에서 무시되기 때문입니다.
질문
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; }
// 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(); } }
AbstractList에서 구현한 removeRange의 경우 protected로 가장 기본적인 삭제 동작을 하기 때문에 하위 클래스에서 성능적 이점을 위해 메서드 재정의를 할 수 있다고 생각하면 된다.
References::
이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)
Loading Comments...