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

2021-12-19 17:10:14

#Java#Effective Java 3/E

상속용 클래스의 문서화

  • 상속용 클래스는 overriding할 수 있는 method들을 내부적으로 어떻게 이용하는 지 문서로 남겨야 한다.
  • 클래스의 API로 공개된 method에서 클래스 자신의 또 다른 method를 호출할 수도 있는데, 이 method가 overriding이 가능하다면 method의 API 설명에 적어두어야 한다.
  • 어떤순서로 호출되는지, 호출 결과가 이어지는 처리에 어떤 영향을 줄 수 있는지도 문서화해야한다.

문서화 예시 : https://docs.oracle.com/javase/7/docs/api/java/util/AbstractCollection.html

API 문서의 method설명 끝에서 종종 "This Implementation" ~ 로 시작하는 절을 볼 수 있는데 이 부분이 method의 내부 동작 방식을 설명하는 곳이다.

클래스를 안전하게 상속할 수 있도록 하려면, 내부 구현 방식에 대해 설명해주어야 한다.

( Java method 주석에 @implSpec tag를 붙여주면 자바독 도구가 생성해준다. )

상속용 클래스의 제약

  1. 상속용 클래스의 생성자는 overriding 가능한 method를 호출해서는 안된다. (private,final,static method는 overriding이 불가능하니 생성자에서 호출하여도 무관하다. )
public class Super {

    public Super(){
        overrideMe();
    }
    public void overrideMe(){ }
}

Super라고 하는 상위 class에서 생성자에 overriding가능한 method를 호출한 상황을 가정하자

public final class Sub extends Super{

    private final Instant instant;

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

    public void overrideMe(){
        System.out.println("instant = " + instant);
    }


    public static void main(String[] args) {
        Sub sub = new Sub();
        sub.overrideMe(); 
        // 실행결과 
        // instant = null
        // instant = 2021-12-19T08:58:46.412929Z
    }
}

Super class를 상속한 자식 class에서 부모 생성자를 호출 -> 부모 생성자에서 overrideMe() 호출 -> 자식class의 instant 필드는 초기화 되기전임으로 null 출력이 된다.

  1. clone과 readObject 모두 overriding 가능한 method를 호출해서는 안된다.

예를 들어 , clone과 readObject는 객체를 새로 만드는 데 중간에 overriding 한 method를 호출한 경우, 제대로 복사되지 않은 객체가 만들어질수 있다.

public class Parent implements Cloneable{

    public Thing[] things;

    @Override
    protected Parent clone() {
        try{
            Parent copyedParent = (Parent) super.clone();
            // 부모 class에서 가변필드를 복사하기전에 overriding가능한 method 호출 
            overridedMethod();
            copyedParent.things = things.clone();
            return copyedParent;
        }
        catch(CloneNotSupportedException e){
            throw new AssertionError();
        }
    }

    protected void overridedMethod(){}
}
public final class Child extends Parent {
    @Override
    protected void overridedMethod() {
        throw new RuntimeException();
    }
}
    public static void main(String[] args) {
        Parent child = new Child();
        child.clone(); // RuntimeException 
    }

특히 clone이 잘못되면 복제본 뿐 아니라 원복객체에도 피해를 줄 수 있다, 가변필드는 복제본이랑 원본과 공유되고 있는 상태를 예시로 들 수 있을 것이다.

  1. Serializable 을 구현한 상속용 class가 readResolve 나 writeReplace method를 갖는다면 이 method들은 private 가 아닌 protected 로 선언해야 한다.
  • readResolve : 역직렬화 과정에서 호출되는 method
  • writeReplace : 직렬화된 상태에서 다시 객체로 만들떄 호출되는 method

private 로 선언한다면 하위 class에서 무시되기 떄문이다.

일반적인 구체 클래스

  • 상속용으로 만들지 않고, 문서화되지 않은 class는 개발할떄 필요에 의해서 상속해도 괜찮을까? 되도록이면 상속을 금지하고, 대신 핵심 기능들을 정의해놓은 인터페이스 구현을 하는 것을 권고하고 있다.

  • 상속을 금지하는 방법은 이전 item에서도 다루었듯이 class를 final로 선언하거나 생성자를 하위class에서 호출할수 없도록 막는 방법이 있다.

  • 꼭 상속을 해야 한다면, 상속할 class 내부에 overriding 가능한 method들을 호출하는 코드가 없도록 하고, 이를 문서화 해야한다.

    public void doSomething(){
        doAnotherThing();
    }

    public void doAnotherThing(){
        // overriding 가능한 method를 호출하고 있음으로, 
        // 이를 private로 변경하고 문서화 해야한다. 
    }

overriding 가능한 자기 사용 코드를 완벽히 제거하면, method를 자식 class에서 overriding해도 다른 method와는 독립적으로 작동할수 있으므로, 안전하다.

정리

상속용 class를 설계하려면 내부동작 방식과 다른 method들을 어떻게 호출하는지 (자기 사용 패턴) 을 모두 문서화해야 하며, 한번 문서화하면 변경하면 안된다. 때문에 상속을 사용해야할 명확할 이유가 없다면 상속을 금지하는 것을 권고한다.

프로필 이미지
@chani
바둑 좋아하는 개발자의 의미있는 학습 기록을 위한 공간입니다.

댓글

이 게시글에 대한 의견을 공유해주세요!

댓글