Yomni's TIL Help

아이템 19. 상속을 고려해 설계하고 문서화하라

1. 상속은 강력하지만 위험하다

  • 상속은 캡슐화를 깨뜨린다.

  • 하위 클래스는 상위 클래스의 구현 세부사항에 의존하기 쉽고,


    이는 깨지기 쉬운 구조를 낳는다.

예시) 상위 클래스 내부의 어떤 메서드가 다른 메서드를 호출하는 방식이 바뀌면,
하위 클래스에서 오버라이드한 메서드가 예기치 않게 호출될 수 있다.

2. 상속을 허용하려면, 반드시 문서화하라.

  • 재정의(overriding)를 허용할 메서드는 그 의도를 명확히 문서화해야 한다.

/** * 이 메서드는 서브 클래스에서 호출될 수 있다. * 재정의 시 super.method()를 반드시 호출해야 한다. */ protected void doSometing() {/*...*/}
  • 재정의가 가능하다는 의미는 단순히 protected 로 선언하는 것만이 아니다.


    재정의 해도 정상적으로 동작할 수 있도록 보장된 계약(contract) 이 있어야 한다.

3. 하위 클래스에서 사용할 수 있도록 설계된 메서드는 확실히 알려야 한다.

  • 재정의 가능한 메서드는 명확한 규약(specification)문서가 필요하다.

  • 그렇지 않으면 하위 클래스는 상위 클래스의 구현을 추측해 사용하게 되고,


    상위 클래스가 변경되면 하위 클래스가 깨질 수 있다.

  • 이런 메서드는 재정의용으로 설계된 메서드 여야 하며, 그 목적에 맞게 작성되고 테스트되어야 한다.

4. 생성자에서는 재정의 가능한 메서드를 호출하지 마라.

  • 생성자에서 this 를 참조하는 메서드는 하위 클래스의 오버라이드된 메서드가 먼저 실행될 수 있다.

public class Super { public Super() { overrideMe(); // 위험 : 아직 하위 클래스 초기화 전 } public void overrideMe() { } } public final class Sub extends Super { private final Date date; public Sub() { date = new Date(); } @Override public void overrideMe() { System.out.println(date); // NullPointerException 발생 가능 } }
  • Sub의 생성자가 실행되기도 전에 overrideMe() 가 호출되면서


    date 필드가 초기화되기 전에 접근 --> NullPointerException

5. 상속을 안전하게 지원하려면, 이를 염두에 두고 처음부터 설계해야 한다.

  • 상속 가능한 클래스는 반드시 문서화하고, 테스트 가능한 방법으로 확장 가능하게 설계되어야 한다.

  • 또는 상속을 아예 금지할 수도 있다.

public final class MyClass {/* ... */ } // 또는 생성자를 private 으로 막고 static factory 만 제공 private MyClass() { } public static MyClass of(/*...*/) {/*...*/}

6. 결론

  • 상속은 매우 강력하지만 위험한 도구(강결합)


    제대로 설계하고 문서화하지 않으면 깨지기 쉬운 API가 된다.

  • 상속을 허용하려면, 정확한 명세, 계약, 문서화, 테스트가 선행되어야 한다.

  • 대부분의 경우에는 컴포지션을 기본 전략으로 삼고, 정말 필요한 경우에만 원칙(문서화, 계약, ..) 에 따라 상속을 허용해야 한다.

7. 비고) 상속 체크리스트

설계

  • 이 클래스는 다른 곳에서 기능 확장을 위해 상속할 필요가 명확한가?

  • is-a 관계가 진정으로 성립하는가?

  • 상속보다 컴포지션으로 더 좋은 구조는 아닌가?

문서화

  • 오버라이드 할 수 있는 메서드는 사용 방법과 규약(contract) 이 명확히 문서화 되어 있는가?

  • 하위 클래스가 호출할 수 있는 protected 메서드나 생성자는 적절히 설계되었는가?

테스트

  • 오버라이드 가능한 메서드가 호출되는 시점이나 방식에 대해 정확히 테스트 되었는가?

  • 하위 클래스에서 발생할 수 있는 예외적인 케이스도 방어할 수 있는가?

생성자

  • 생성자에서는 재정의 가능한 메서드를 호출하는 부분이 없는가?

  • 초기화 순서 문제로 인해 하위 클래스가 깨질 가능성은 없는가?

느낀점

  • 템플릿 메서드 패턴(Template Method Pattern)

    • 무려 GoF 가 설계한 패턴으로, '템플릿 메서드 패턴'은 상속을 적극 활용하는 디자인 패턴이다.

    • 책에서는 직접적으로 언급하지 않지만, 상속을 허용하고자 할 때는 템플릿 메서드 패턴을 적용하는 것이 한 가지 전략이 될 수 있다.

    • 템플릿 메서드 패턴 : 알고리즘의 구조 (고정된 흐름)는 정해놓고, 하위 클래스에서 특정 단계만 오버라이드하게 유도하는 디자인 패턴

  • 추상 클래스 vs 인터페이스 + 기본 구현 클래스 조합

    • 상속을 허용하고자 할 때, 반드시 추상 클래스를 사용해야 하는 것은 아닌 것 같다.

    • 더 안전하고 유연한 방법은

      • 인터페이스 정의

      • 기본 구현 제공 클래스(composition 사용)

      • 확장 필요 시, 구현 클래스 내에서 delegation 사용

  • 상속보다는 컴포지션을 우선하라..

    • item 18 의 내용을 최우선으로 생각해야되는 건 맞는 것 같다.

    • 상속을 피할 수 없는 경우, 상속을 위한 원칙을 체크리스트로 만들어 보는 것도 좋을 듯 하다.

  • kotlin 에서는 이런 원리 원칙을 어떻게 적용할까?

  • 실무에선 상속이 있다면 어떻게 리팩토링 할 수 있을까..?

Last modified: 09 April 2025