Item 19. Inheritance

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

상속의 위험성

앞의 아이템에서 다뤘듯이 상속은 코드를 재사용하기 위해 유용한 도구이지만, 항상 최선이 아니며, 잘못 사용하게되면 오류를 내기 쉽게 된다. 때문에 클래스를 설계할 때 상속을 고려해야하고 각 메서드마다 어떤 식으로 동작할지 상세하게 문서화해야 한다. 만약 상속을 하기에 부적절한 클래스라면 상속을 금지하는 조치를 취해야 한다.

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

상속을 고려한 설계와 문서화를 하고자 한다면 아래의 사항들을 지키는 것이 좋다.

1. @implSpec을 통한 문서화

주석에 @implSpec 태그를 추가하여 Implentation Requirements 내용을 문서화하여 클라이언트에게 정보를 제공해 줄 수 있다.

/**
 * {@inheritDoc}
 *
 * @implSpec 이 구현은 다음과 같다.
 * {@code
 *  return 1;
 * }
 */

2. protected를 이용한 메서드 공개

클래스의 내부 동작 과정 중간에 끼어들 수 있는 훅을 잘 선별하여 protected 메서드 형태로 공개하여 큰 어려움 없이 효율적인 하위 클래스를 만들 수 있도록 할 수 있다. 실제로 java.util.AbstractListremoveRange 메서드는 protected로 선언되어 있다. List 구현체를 사용하는 클라이언트는 해당 메서드의 존재를 모르지만, AbstractList를 상속한 하위 클래스는 해당 메서드를 사용하여 효율적으로 clear 메서드를 구현할 수 있게 된다.

protect의 공개 여부는 정확히 정해진 것은 없으며, 하위 클래스를 만들어보고 테스트해보면서 적절한지 판단해야 한다. 하지만 많은 메서드를 노출하면 캡슐화가 어려워지고, 적게 노출하면 하위 클래스를 만들기 어려워진다는 점을 명심해야 한다.

3. 상속용 클래스의 생성자

상속용 클래스의 생성자는 직/간접적으로 재정의 가능 메서드를 호출해서는 안된다. 자세한 예시 및 이유는 아래의 코드를 참고하자.

import java.time.Instant;

class Super {

    public Super() {
        overrideMe(); // 3. overrideMe() 호출
    }

    public void overrideMe() {
    }
}

// 하위 클래스
final class Sub extends Super {

    private final Instant instant;

    Sub() {
        super(); // 2. 상위 클래스의 생성자 호출
        instant = Instant.now(); // 6. instant 초기화
    }

    // 4. 상위 클래스에서 호출 받아 오버라이딩 된 메서드 실행
    public void overrideMe() {
        System.out.println(instant); // 5. 아직 초기화되지 않은 instant 출력
    }
}

class Main {
    public static void main(String[] args) {
        Sub sub = new Sub(); // 1. 생성자 호출
        sub.overrideMe(); // 7. overrideMe() 호출하여 정상적으로 instant 출력
    }
}

예시 코드에서는 System.out.printlnnull을 에러 없이 실행시키지만, 만약 다른 메서드를 호출한다면 NullPointerException이 발생하게 될 수 있다. 때문에 생성자 내부에서는 private, final, static 같은 재정의 불가능한 메서드만 호출해야 한다.

4. Cloneable, Serializable 인터페이스

CloneableSerializable 인터페이스를 구현한 클래스는 상속용으로 설계하기에 적합하지 않으며, 일반적으로 권장하지 않는다. Cloneableclone 메서드를, SerializablereadObject 메서드를 각각 구현해야 하기 때문에 하위 클래스에서는 이를 구현해야 하기 때문에 확장의 부담이 더 커지게 된다.

clonereadObject 메서드는 생성자와 비슷한 효과(새로운 인스턴스 생성)를 내기 때문에, 만약 구현 해야한다면 마찬가지로 직/간접적으로 재정의 가능 메서드를 호출해서는 안된다.

5. 상속용 클래스의 직렬화

Serializable을 구현한 상속용 클래스가 readResolve 혹은 writeReplace 메서드를 선언하는 경우 protected로 선언해야 한다. (private으로 선언하면 하위 클래스에서는 해당 메서드를 사용할 수 없게 된다.)

상속 금지

위에서 보았듯이 상속 가능한 클래스를 설계하기 위해선 많은 노력이 필요하기 때문에 상속을 금지하는 것이 더 좋을 수 있다. 상속을 금지하는 방법은 아래 두 가지가 있다.

  • 클래스를 final로 선언

  • 모든 생성자를 private 혹은 package-private으로 선언하고, public 정적 팩터리를 제공

현실적인 대안

사실 많은 개발자들이 철저한 설계 안에서 개발 할 수 없기 때문에, 위의 원칙을 지키는 것은 현실적으로 어렵다. 때문에 완벽한 설계가 아니더라도 무조건 상속을 금지하는 것이 아니라 클래스 내부에서 재정의 가능 메서드를 사용하지 않게 만들고, 해당 사실을 문서화하는 정도만 하더라도 오동작을 방지할 수 있다.

Last updated

Was this helpful?