ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 19. 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라
    백수의 개발/이펙티브 자바 2019. 10. 5. 12:03

    상속을 위한 문서화

    상속용 클래스는 재정의할 수 있는 메서드들을 내부적으로 어떻게 이용하는지 남겨야한다.

    클래스의 API로 공개된 메서드에서 클래스 자신의 또 다른 메서드를 호출할 수 있다.

    이 때 호출하는 메서드가 재정의 가능 메서드라면 그 사실을 호출하는 메서드의 API 설명에 적시해야 한다. 덧붙여 어떤 순서로 호출하는지, 각각의 호출 결과가 이어지는 처리에 어떤 영향을 주는지도 담아야 한다.

    * API 문서의 메서드 설명 끝에 "Implementation Requirements"라고 되어 있다면, 그 메서드의 내부 동작 방식을 설명하는 것이다.

     

    아래는 java.util.AbstactCollection API문서의 remove 메서드이다.

    해당 메서드의 기능에 대해 우선 이야기를 하고, implementaion을 통해 어떻게 동작이 되는지 설명하고 있다.

    내용을 조금 읽어보면 iterator를 사용하는 것을 알 수 있는데, 이는 iterator 메서드를 재정의하면 remove 메서드의 동작에 영향을 줄 수 있다는 것을 알 수 있다.

     

    "좋은 API 문서란 '어떻게'가 아닌 '무엇'을 하는지를 설명해야 한다."라는 말과 대치된다. 그러나 클래스를 안전하게 상속할 수 있도록 하려면 내부 구현방식을 위와 같이 설명해주어야만 한다.

     

    상속용 클래스 테스트

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

    다양한 하위 클래스를 작성해보면서 전혀 쓰이지 않는 protected 멤버는 private이었어야 할 가능성이 크고, 꼭 필요했을 protected 멤버를 놓쳤다면 확연히 드러날 것이다.

    3개 정도의 하위 클래스를 통해 검증을 하면 적당할 것이고, 이 중 하나 이상은 제 3자에 의해 작성되어봐야 할 것이다.

     

    상속 허용 클래스의 제약

    1. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다.
    2. private, final, static 메서드는 재정의가 불가능하니 생성자에서 안심하고 호출해도 된다.
    3. clone과 readObject 모두 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안 된다. (생성자와 비슷한 효과를 낸다.)
    4. Serializable을 구현할 때 readResolve나 writeReplace 메서드는 private이 아닌 protected로 선언해라.
      1. private로 선언한다면 하위 클래스에서 무시되어 문제가 생긴다.

    생성자가 재정의 기능 메서드를 호출 할 때 어떤 문제가 생기는지 아래 코드를 통해 알아보자.

    public class Super {
        public Super() {
            overrideMe();
        }
    
        public void overrideMe() {
        }
    }
    public final class Sub extends Super {
        private final Instant instant;
    
        Sub() {
            instant = Instant.now();
        }
    
        @Override public void overrideMe() {
            System.out.println(instant);
        }
    }
    
    Sub sub = new Sub();
    sub.overrideMe();

    Sub인스턴스를 생성할 때 상위의 Super의 생성자가 먼저 호출되고, 그 후 Sub의 생성자가 호출된다.

    그래서 Super의 overrideMe()와 sub.overrideMe() 총 2번이 호출되게 된다. 그 결과 instant를 2번 출력할 것이라고 생각할 수 있다.

    그러나 overrideMe가 재정의 된 것을 잘 호출하지만, Sub의 생성자가 실행되기 전에 overrideMe 메서드를 호출하기 때문에 instant가 초기화 되지 않은 상태로 실행이 된다.

    그 결과 Super에서 호출 된 overrideMe에서는 null을 출력하게 되고, sub.overrideMe()에서만 올바른 출력이 이루어진다.

     

    이것이 상위 클래스의 생성자가 재정의 가능한 메서드를 호출해 오작동을 일이키는 간단한 예시이다.

     

    상속용이 아니라면 상속을 금지해라

    위의 다양한 문제들을 해결하는 가장 좋은 방법은 상속용으로 설계하지 않은 클래스는 상속을 금지하는 것이다.

     

    상속을 금지하는 방법은 크게 두 가지다.

    1. 클래스를 final로 선언하라.
    2. 모든 생성자를 private나 package-private로 선언하고, public 정적 팩터리를 만들어주어라.
      1. 내부에서 다양한 하위 클래스를 만들어 쓸 수 있는 유연성을 준다.

    상속을 금지하더라도 Set, List, Map처럼 상속이 불가능하더라도 개발에 큰 어려움 없이 제공가능하다.

    래퍼 클래스 패턴 역시 기능을 확장할 때 상속 대신 쓸 수 있는 더 나은 대안이다.

     

    클래스 상속을 허용해야 한다면

    구체 클래스가 표준 인터페이스를 구현하지 않았는데 상속을 금지하면 사용하기에 상당히 불편해진다.

    이 때 상속을 허용하기 위해 아래와 같이 해보자.

     

    1. 클래스 내부에서는 재정의 가능 메서드를 사용하지 않게 만들고, 이를 문서에 작성해라.
      1. 재정의 가능 메서드를 호출하는 자기 사용 코드를 완벽히 제거해라.
    2. 재정의 가능 메서드를 사용하는 코드를 제거해라.
      1. 재정의 가능 메서드는 자신의 본문 코드를 private으로 옮기고, 이를 호출하도록 수정해라.

     

    마무리

    상속용 클래스를 설계하는 것은 굉장히 어렵다.

    클래스 내부에서 스스로를 어떻게 사용하는지 문서화 해야하며, 문서화한 것은 그 클래스가 쓰이는 한 반드시 지켜야 한다.

    다른 이가 효율 좋은 하위 클래스를 만들 수 있도록 일부 메서드를 protected로 제공해야할 수 있다. 그러나 명확한 이유가 없다면 상속을 금지하는 것이 좋을 수 있다.

    상속을 금지하기 위해서 클래스를 final로 선언하거나, 생성자 모두 private또는 package-private로 선언외부 접근을 제한해라.

    댓글

Designed by Tistory.