peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라. #19

Open pyeong114 opened 6 months ago

pyeong114 commented 6 months ago

Chapter : 4. 클래스와 인터페이스

Item : 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라.

Assignee : eunpyeong114


🍑 서론

🍑 본론

상속을 고려한 설계와 문서화란?

: 매서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리. : 상속용 클래스는 재정의할 수 있는 매서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야 한다. : 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야 한다.

API 문서의 메서드 설명 끝에 종종 "Implementation Requirements"로 시작하는 적을 볼 수 있는데, 그 메서드의 내부 동작 방식을 설명하는 곳이다. 이 절은 매서드 주석에 @implSpec 태그를 붙여주면 자바독 도구가 생성해준다. @implSpec 태그는 자바 8에서 처음 도입되어 자바 9부터 본격적으로 사용되기 시작했다.

재정의 가능 매서드란? public과 protected 메서드 중 final이 아닌 모든 메서드를 뜻함

private, final, static 메서드는 재정의가 불가능하니, 생성자에서 안심하고 호출해도 된다.

효율적인 하위 클래스를 큰 어려움 없이 만들 수 있게 하려면 클래스의 내부 동작 과정 중 간에 끼어들 수 있는 훅(hook)을 잘 선별하여 protected 메서드 형태로 공개해야 할 수도 있다.

java.util.AbstractList의 removeRange 메서드를 예로 살펴보자

protected void removeRange(int fromIndex, int toIndex)

fromIndex(포함)부터 toIndex(미포함)까지의 모든 원소를 이 리스트에서 제거한다. toIndex 이후의 원소들은 앞으로 (index만큼씩) 당겨진다. 이 호출로 리스트는 'toIndex - fromIndex' 만큼 짧아진다.
 이 리스트 혹은 이 리스트의 부분리스트에 정의된 clear 연산이 이 메서드를 호출한다.
리스트 구현의 내부 구조를 활용하도록 이 메서드를 재정의하면 이 리스트와 부분리스트의 clear 연산 성능을 크게 개선할 수 있다.
  **Implementation Requirements**: 이 메서드는 fromIndex에서 시작하는 리스트 반복자를 얻어 모든 원소를 제거할 때까지 ListIteratore.next와 ListIterator.remove를 반복 호출하도록 구현되었다. 
주의: **ListIterator.remove가 선형시간이 걸리면 이 구현의 성능은 제곱에 비례한다**

Parameters:
  fromIndex  제거할 첫 원소의 인덱스
  toIndex      제거할 마지막 원소의 다음 인덱스
...

List 구현체의 최종 사용자는 removeRange 메서드에 관심이 없다. 그럼에도 이 메서드를 제공한 이유는 단지 하위 클래스에서 부분리스트의 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서다. removeRange 메서드가 없다면 하위 클래스에서 clear 메서드를 호출하면 제곱에 비례해 성능이 느려지거나 부분리스트의 메커니즘을 밑바닥부터 새로 구현해야 했을 것이다.

상속용 클래스를 설계할 때 어떤 메서드를 protected로 노출해야 할지 어떻게 결정할까?

심사숙고해서 잘 예측해 본 다음, 실제 하위 클래스를 만들어 시험해보는 것이 최선이다. protected 메서드 하나하나가 내부 구현에 해당하므로, 그 수는 가능한 한 적어야 한다. 한편으로는, 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야 한다. => 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야 한다.

상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 매서드를 호출해서는 안된다.

public class Super{
  // 잘못된 예 - 생성자가 재정의 가능 메서드를 호출
  public Super(){
      overrideMe();
  }

  public void overrideMe(){
  }
}
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();
  }
}

이 프로그램이 instant를 두 번 출력하리라 기대했겠지만, 첫 번째는 null을 출력한다. 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출하기 때문!

OverrideMe에서 instant 객체의 메서드를 호출하려 한다면 상위 클래스의 생성자가 overrideMe를 호출할 때 NullPointerException을 던지게 된다. 이 프로그램이 NullPointerException을 던지지 않은 유일한 이유는 println이 null 입력도 받아들이기 때문

private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.

상속용으로 설계하지 않은 클래스는 상속을 금지하라!

상속을 금지하는 방법 2가지

1. 클래스를 final로 선언하는 방법

2. 모든 생성자를 private나 package-private으로 선언하고 public 정적 팩터리를 만들어주는 방법

: 생성자 모두를 외부에서 접근할 수 없도록 만드는 것 : 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성이 생김 : 아이템 17에서 다뤘음

🍑 결론

상속용 클래스를 설계하기란 결코 만만치 않다.

클래스 내부에서 스스로를 어떻게 사용하는지 모두 문서로 남겨야 하며, 일단 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다. 그렇지 않으면 그 내부 구현 방식을 믿고 활용하던 하위 클래스를 오동작하게 만들 수 있다. 클래스를 확장해야 할 명확한 이유가 떠오르지 않으면 상속을 금지하는 편인 나을 것이다.


Referenced by

-

Lainlnya commented 6 months ago
Screenshot 2024-03-18 at 9 35 52 PM