[아이템 19] 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라

gang_shik·2022년 4월 16일
0

Effective Java 4장

목록 보기
5/11
  • 아이템 18에서는 상속을 염두에 두지 않고 설계했고 상속할 때의 주의점도 문서화해놓지 않은 외부 클래스를 상속할 때의 위험을 경고함(외부란, 프로그래머의 통제권 밖에 있어서 언제 어떻게 변경될지 모른다는 뜻임)

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

  • 메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야 함

  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지(자기사용) 문서로 남겨야함

    • 클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수도 있음, 그런데 마침 호출되는 메서드가 재정의 기능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야함

    • 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 함(재정의 가능은 publicprotected 메서드 중 final 이 아닌 모든 메서드를 뜻함)

  • 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야함

  • 클래스를 안전하게 상속할 수 있도록 하려면(상속만 아니었다면 기술하지 않았어야 할) 내부 구현 방식을 설명해야만 함

  • 내부 매커니즘을 문서로 남기는 것만이 상속을 위한 설계의 전부는 아님

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

    • 드물게 protected 필드를 공개해야 할 수도 있음
  • 상속용 클래스를 설계할 때 메서드를 protected로 노출하는데 있어서 실제 하위 클래스를 만들어 시험을 해보면서 예측해야함

  • protected 메서드 하나하나가 내부 구현에 해당하므로 그 수는 가능한 한 적어야 함, 한편으로는 너무 적게 노출해서 상속으로 얻는 이점마저 없애지 않도록 주의해야함

  • 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일함

    • 꼭 필요한 protected 멤버를 놓쳤다면 하위 클래스를 작성할 때 그 빈 자리가 드러남

    • 하위 클래스를 여러 개 만들 때까지 전혀 쓰이지 않는 protected 멤버는 사실 private이었어야 할 가능성이 큼

  • 널리 쓰일 클래스를 상속용으로 설계한다면 문서화한 내부 사용 패턴과, protected 메서드와 필드를 구현하면서 선택한 결정에 영원히 책임져야함

  • 상속용으로 설계한 클래스는 배포 전에 반드시 하위 클래스를 만들어 검증해야함

훅(hook)?

훅(Hook)

훅(Hook)은 추상 클래스에 들어있는 아무 일도 하지 않거나 기본 행동을 정의하는 메소드로 서브 클래스에서 오버라이딩 할 수 있다는게 정의임

이것을 잘 선정하라는 것은 책의 예시를 보면 알 수 있음

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();
		}
}

여기서 상위 클래스의 생성자 역시 불려질 때 overrideMe 역시 호출되는데 이 메소드를 보면 재정의를 해서 채울 수 있지만 그 전에 아무일도 하지 않는 메소드임

위의 상황이 내부 동작 과정 중간에 훅이 끼어들 수 있다는 것을 의미함, 실제로 위의 예시 overrideMe 메소드가 아무것도 하지 않기 때문에 상위 클래스의 생성자는 null로 출력이 됨, 이런 부분에 대해서 잘 파악학고 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();
      		}
      }
    • 위와 같이 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe를 호출해서 처음엔 null을 출력함

    • println이 null 입력도 받아들여서 NullPointerException을 던지지 않음

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

  • CloneableSerializable 인터페이스는 상속용 설계의 어려움을 한층 더해줌, 둘 중 하나라도 구현한 클래스를 상속할 수 있게 설계하는 것은 일반적으로 좋지 않은 생각임, 그 클래스를 확장하려는 프로그래머에게 엄청난 부담을 지움

  • clonereadObject 메서드는 생성자와 비슷한 효과를 냄(새로운 객체를 만듬)

  • 상속용 클래스에서 CloneableSerializable 을 구현할지 정해야 한다면, 이들을 구현할 때 따르는 제약도 생성자와 비슷하다는 점에 주의해야함

  • clonereadObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안됨

    • readObject의 경우 하위 클래스의 상태가 미처 다 역직렬화되기 전에 재정의한 메서드부터 호출하게 됨

    • clone 의 경우 하위 클래스의 clone 메서드가 복제본의 상태를 (올바른 상태로) 수정하기 전에 재정의한 메서드를 호출함

    • 어느 쪽이든 프로그램 오작동으로 이어질 것임

    • clone 의 경우 원본 객체에도 피해를 줄 수 있음

  • Serializable을 구현한 상속용 클래스가 readResolvewriteReplace 메서드를 갖는다면 이 메서드들은 private 이 아닌 protected 로 선언해야함

일반 구체 클래스

  • 이런 클래스는 final 도 아니고 상속용으로 설계되거나 문서화되지도 않음

  • 그대로 두면 클래스에 변화가 생길 때마다 하위 클래스를 오동작하게 만들수 있기 때문임

  • 이 때 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것임

상속을 금지하는 방법

  • 클래스를 final 로 선언하는 것

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

  • 아이템 18에서 설명한 래퍼 클래스 패턴 역시 기능을 증강할 때 상속 대신 쓸 수 있는 더 나은 대안임

만약 꼭 해야 한다면

  • 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고 이 사실을 문서로 남기는 것

  • 재정의 가능 메서드를 호출하는 자기 사용 코드를 완벽히 제거하는 것임

  • 클래스의 동작을 유지하면서 재정의 가능 메서드를 사용하는 코드를 제거할 수 있는 기계적인 방법은 먼저 각각의 재정의 가능 메서드는 자신의 본문 코드를 private 도우미 메서드로 옮기고, 이 도우미 메서드를 호출하도록 수정함

  • 그런 다음 재정의 가능 메서드를 호출하는 다른 코드들도 모두 이 도우미 메서드를 직접 호출하도록 수정하면 됨

profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글