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

반응형
728x90
반응형

들어가기전

아이템 19를 공부하기 전, 아이템 18 포스팅을 먼저 읽어보자.

https://devfunny.tistory.com/544

 

[교재 EffectiveJava] 아이템 18. 상속보다는 컴포지션을 사용하라

상속 상속은 코드를 재사용하는 강력한 수단이지만, 항상 최선은 아니다. 잘못 사용하면 오류를 내기 쉬운 소프트웨어를 만들게된다. 안전한 상속 상위 클래스와 하위 클래스를 모두 같은 프로

devfunny.tistory.com

 

 

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

우선, 메서드를 재정의하면 어떤 일이 일어나는지를 정확히 정리하여 문서로 남겨야한다. 즉, 상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 문서로 남겨야한다.

1) 클래스의 API 로 공개된 메서드에서 클래스 자신이 또다른 메서드를 호출할 수도 있다.
만약 호출되는 메서드가 재정의 가능 메서드라면 그 사실을 호출 메서드의 API 설명에 적어야한다.


2) 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야한다.
** 재정의 가능 : public, protected 메서드 중 final 이 아닌 모든 메서드

3) 재정의 가능 메서드를 호출할 수 있는 모든 상황을 문서로 남겨야한다.

API 문서를 보면 메서드 설명 끝에 종종 "Implementation Requiredments" 로 시작하는 절을 볼 수 있는데, 이 부분이 그 메서드의 내부 동작 방식을 설명하는 곳이다. 문서화에 사용 설명 및 주의점을 적어놓은 이유는 상속이 캡슐화를 해치기 때문에, 클래스를 안전하게 상속하게하기 위함이다. 

 

 

protected 메서드 공개

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

 

protected 접근제한자

소속된 패키지 안의 모든 클래스에서 접근할 수 있다. 상속 관계의 하위 클래스에서도 접근할 수 있다.

 

예를 들어 java.util.AbstractList 의 removeRange 메서드는 부분 리스트의 clear 메서드를 고성능으로 만들기 쉽게하기 위해서 protected 로 공개된 메서드다.

protected void removeRange(int fromIndex, int toIndex) {
    ListIterator<E> it = listIterator(fromIndex);
    for (int i=0, n=toIndex-fromIndex; i<n; i++) {
        it.next();
        it.remove();
    }
}

 

protected 노출 여부의 결정은 잘 예측해본 다음, 실제 하위 클래스를 만들어 시험해보는 것이 최선이다. 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 유일하다. 하위 클래스를 여러개 만들때까지 전혀 쓰이지 않는 protected 멤버는 사실 private 였어야할 가능성이 크다. 이렇게 직접 확인하는 방법은 여러 하위 클래스를 만들어보는 수밖에 없다.

 

 

상속을 허용하는 클래스가 지켜야할 제약

상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다. 상위 클래스의 생성자가 하위 클래스의 생성자보다 먼저 호출되므로 하위 클래스에서 재정의한 메서드가 하위 클래스의 생성자보다 먼저 호출된다. 이때 재정의한 메서드가 하위 클래스의 생성자에서 초기화하는 값에 의존한다면 의도대로 작동되지 않을 것이다.

 

Super.java (상위클래스)
package com.java.effective.item19;

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

    public void overrideMe() {

    }
}

 

Sub.java (하위클래스)
package com.java.effective.item19;

import java.time.Instant;

public class Sub extends Super {
    /* 생성자에서 초기화되는 필드 */
    private final Instant instant;
    public Sub() {
        instant = Instant.now(); /* 생성자 호출시 초기화 */
    }

    @Override
    public void overrideMe() {
        System.out.println(instant);
    }

    public static void main(String[] args) {
        /**
         * 기댓값: 2번 호출
         * (상위클래스의 생성자에서 호출되는 overrideMe(); 로 인해 1번,
         * 그 다음 아래 sub.overrideMe(); 로 인해 2번.
         *
         * 결과값: 첫번째에 null 호출, 그 다음에 정상 호출.
         * 상위 클래스의 생성자는 하위 클래스의 생성자가 인스턴스 필드를 초기화하기도 전에 overrideMe 를 호출한다.
         */
        Sub sub = new Sub();
        sub.overrideMe();
    }
}

 

상위 클래스의 생성자가 먼저 호출된다. 하위 클래스의 생성자에서 인스턴스 필드 instant 를 초기화하기 때문에 그 전에 상위 클래스의 생성자에 의해 호출된 overrideMe() 시점의 instant 는 null 값이다. overriteMe() 에서 instant 객체의 메서드를 호출하려 한다면 NullPointerException 을 던지게 될 것이다. 이는 명백한 오류다.

 

 

상속을 금지하는 방법

상속용으로 설계하지 않은 클래스는 상속을 금지해야한다.

 

1. 클래스를 final 로 선언한다. 

final public class Test {
	...
}

 

2. 모든 생성자를 private 나 package-private 로 선언하고 public 정적 팩터리를 만들어준다.

@Getter
public class TestDto {
    private int sum;
    
    private TestDto() {}
    private TestDto(int index) {
        this.index = index;
    }
    
    public static TestDto getInstance() {
        return new TestDto();
    }
}

 

반응형

Designed by JB FACTORY