티스토리 뷰

728x90
반응형

상속을 고려해 설계하고 문서화하라. 그렇지 않다면 상속을 금지하라


  • 상속을 염려해두지 않은 외부 클래스(프로그래머의 통제권 밖에 있어 변경시점을 알 수 없는 클래스)를 상속할 경우 여러 문제가 발생할 수 있습니다. 그렇기 때문에 상속용 클래스를 설계할 때는 문서화가 중요합니다.

 

상속을 고려한 설계와 문서


  • 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용되는지 문서로 남겨야 합니다.
  • 호출된 메서드가 재정의 가능한 메서드라면 즉, 하위 클래스의 메서드라면 그 사실을 호출한 메서드의 API에 설명을 적시해야 합니다. 더불어 어떤 순서로 호출하는지, 각각의 호출 결과가 어떻게 이어지는지, 처리는 어떻게 되는지 등 모든 상황을 적시해야 합니다.
  • 여기서 재정의 가능 메서드란 public, protected 메서드 중 final이 아닌 모든 메서드를 말합니다.

 

💡 Implementation Requirements

  • API 문서를 보면 implementation Requirements라는 항목이 존재하는데, 이 메서드의 내부 동작 방식을 설명하는 절입니다. 이 메서드 주석에 @impleSpec 태그를 붙혀주면 자바독 도구가 생성해줍니다.

java.util.AbstractList

 

💡 좋은 API문서란?

  • 좋은 API 문서란 어떻게가 아닌 무엇을 하는지를 설명해야 합니다. 
  • 이 주제는 바로 위에서 작성된 API 문서의 내용과는 정 반대입니다. 왜 그럴까요? 그 이유는 상속이 캡슐화를 깨트리기 때문인데, 클래스를 안전하게 상속할 수 있도록 하기 위해서는 내부 구현 방식을 설명해야만 하기 때문입니다.

 

💡 HOOK

  • 클래스 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별해서 protected 메서드 형태로 공개해야 할 수도 있습니다.
  • 아래의 코드에서 removeRange 메서드를 직접 사용할 일도 없고 관심도 없고 저런 메서드가 있는지 오늘 알게 되었습니다. 그런데 왜 이 메서드가 문서에 제공이 되고 있을까요? 이는 List 구현체 최종 사용자가 아니라 AbstractList의 하위 클래스를 만들 사람이 이 메서드를 사용하는 clear 메서드를 고성능으로 만들기 쉽게 하기 위해서인데 clear 메서드는 내부적으로 removeRange 메서드를 호출하고 있습니다.

java.util.AbstractList

 

💡 Protected 메서드로 노출하기 위한 요건

  • 책에서도 명확한 기준을 제시하지는 않습니다. 그렇기 때문에 실제로 하위 클래스를 만들어 보면서 테스트를 진행해보는것이 좋습니다.

 

🧨 주의점

  • main 메서드에서 Sub 클래스의 overrideMe 메서드를 호출하였습니다. 그리고 Sub 클래스의 생성자에서는 instant 필드를 초기화 했는데 왜 두번의 overrideMe 메서드가 출력되었고, 그 중 첫번째는 왜 null일까요?
    • 그 이유는 하위 클래스의 생성자보다 상위 클래스의 생성자가 항상 먼저 호출됩니다. Sub 클래스의 생성자에서 명시적으로 super 메서드를 호출하지 않아도 상위 클래스의 생성자에 필수 매개변수가 없는 기본 생성자가 있는 경우 묵시적으로 super가 호출됩니다. 그렇기 때문에 부모 클래스인 Super의 생상자가 실행이 되는데 여기서 overrideMe 메서드를 호출합니다. 
    • 그렇게 되면 실제 구현체는 Sub 클래스이기 때문에 재정의된 overrideMe 메서드가 실행이 되고, 이 시점에는 아직 instant가 초기화 되지 않았기 때문에 null이 출력됩니다. 만약 로직이 단순하지 않고 instant를 가지고 핸들링을 해야하는 상황이였다면 NPE가 발생했을 것입니다.
public class Super {

    public Super() {
        overrideMe();
    }

    public void overrideMe() {
        System.out.println("Super override method!");
    }
}


public class Sub extends Super {

    private final Instant instant;

    public Sub() {
        instant = Instant.now();
    }

    @Override
    public void overrideMe() {
        System.out.println("Sub override method! " + instant);
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

// Sub override method! null
// Sub override method! 2022-07-13T13:21:44.549604Z

 

💡 Cloneable, Serializable

  • 직렬화, 객체 복사에 사용되는 clone(), readObject()와 같은 경우도 생성자와 비슷한 효과를 가지고 있으므로 직접적이든 간접적이든 재정의 가능한 메서드를 호출해서는 안됩니다.
  • readObject 메서드는 역직렬화가 끝나기 전에 재정의한 메서드부터 호출하게 됩니다.
  • clone 메서드는 하위 클래스의 clone 메서드가 복제본 상태를 올바른 상태로 수정하기 전에 재정의한 메서드를 호출합니다. 그렇기 때문에 clone 메서드가 잘못되면 원본 객체에도 피해를 줄 수 있습니다.

 

💡 정리

  • 상속용 메서드를 만들때는 클래스 내부에 무엇을 하는지 문서로 남겨야합니다.
  • 문서화한 것은 해당 클래스가 쓰이는 한 반드시 지켜야합니다. 그렇지 않으면 오작동을 발생시킬 수 있습니다.
  • 클래스를 확장해야할 명확한 이유가 없다면 상속을 하지 않는것이 좋습니다. 오히려 독이 됩니다.

 

 

 

 

 

728x90
반응형