본문 바로가기
독서 감상문/Effective Java

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

by 프람6 2024. 5. 2.

작성자: 프람

작성일시: 2024_05_02
내용: Effective java 3/E 아이템 19

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

인사

안녕하세요~😊 프람입니다.
이번 주부터 부쩍 날씨가 더워졌습니다. 일교차도 심한만큼 감기 환자들도 많이 생긴거 같은데요. 다들 화이팅 합시다.

본격적으로 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라에 대해 설명드리겠읍니다.👍

결론

집중력이 부족한 어린이들을 위해 결론부터 말씀드리겠습니다.
앵간하면 상속 금지하고(class final붙히든, private 생성자로 상속 막으라는 뜻),

상속은 기능 확장이 필요할 때만 하세요. (이때는 메서드 오버라이딩 final로 막아주는 것이 좋다.)

그래도 불가피하게 상속을 해야한다면

~

그때는 어떻게 해당 메서드가 구현되는지를 구체적으로 문서로 작성해주시길 추천드립니다.

상속을 피하는 방법은 아이템 18(상속보다는 컴포지션을 사용하라), 아이템 20(추상 클래스보다는 인터페이스를 우선하라 참고 해보아요🤗

상속을 허용하는 곳에서의 문서화

일반적으로 문서화를 하라고 할땐 "어떻게 가 아닌 무엇을 하는지를 설명해야 한다"라고들 하는데,
본 아이템에서 "상속이 허용한 메서드는 문서에 어떻게 구현할 것인지 까지 구체적으로 작성하라"라고 합니다.

그 이유는 무엇인지 하나씩 알아봅시다

  1. 상속을 하고 오버라이드하는 시점에서 이미 캡슐화 박살남😡
  2. 중간에 괴상한 놈들이 끼여 있을 수 있음.😈

첫 번째 캡슐화가 박살난다. 내용은 이미 무슨 내용일지 익히 알고 있을거라고 생각하지만,
구체적인 내용을 원하신다면 , 아이템 18. 상속 보다는 컴포지션을 사용하라 내용을 보고 되살펴 봅시다.❤️

두 번째 괴상한 놈들이 끼여 있을 수 있다는 뭘까요? 책에서는 예시로 AbstractCollection의 remove 메서드를 예시로 들고 있습니다. 같이 코드를 살펴 볼까요?

public abstract class AbstractCollection<E> implements Collection<E> {
    //생략..
    public abstract Iterator<E> iterator();

    //생략..
    public boolean remove(Object o) {  
        Iterator<E> it = iterator();  
        if (o==null) {  
            while (it.hasNext()) {  
                if (it.next()==null) {  
                    it.remove();  
                    return true;  
                }  
            }  
        } else {  
            while (it.hasNext()) {  
                if (o.equals(it.next())) {  
                    it.remove();  
                    return true;  
                }  
            }  
        }  
        return false;  
    }
} 

어떤가요? 조금 복잡해도 자세하게 잘펴봅시다.

추상 클래스에서 이미 구현된 remove가 Iterator()라는 추상메서드의 오버라이드에 의해 로직이 바뀔 수도 있겠다 싶죠?

그렇습니다. 위 코드는 실제로도 iterator()를 잘 구현해주어야 정상 작동합니다.(remove)

상속은 이처럼 생각보다 단순하지 않아요.

그렇기 때문에 아이템 19에서 일반적인 관례를 깨고 상속 시에는

무엇 보다는 어떻게 구현을 했는지를 자세하게 설명하라고 하는 것이죠.👍

*문서화하는 docs용 주석의 태그를 책에서는 소개하는데 (@implSpec) 딱히, 중요한 내용이 아닌듯 싶어 제외합니다

*~~

상속을 고려해 설계를 하려면?

상속을 하려는 코드가 어떤 위험을 내제하고 있는지는 대충 알아본거 같죠? 그렇다면 상속을 고려해서 좋은 설계를 하려면 어떻게 해야할까요?🤔

  1. Hook[1]을 고려하여 설계하라
  2. 상속용 클래스를 시험하는 방법은 직접 하위 클래스를 만들어보는 것이 '유일'하다.
  3. 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.
  4. 상속용으로 설계한 클래스는 배포 전에는 반드시 하위 클래스를 만들어 검증해야한다.

다소 생소한 내용이 있습니다. 그죠? 생소한것만 차근차근 같이 알아봅시다.🤗

 

 


첫째, Hook을 고려하여 설계하라.

우선 Hook이란 무엇인지 코드를 통해 이해해봅시다.

public abstract class AbstractList<E> extends 생략, implements 생략 {
    //생략 ...
    public void clear() {  
        removeRange(0, size());  
    }
    //생략 ...
    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로 removeRange메서드에 대한 정의가 되어있고요.

외부에서 clear를 호출하면,

removeRange가 호출되어 해당 list가 지워지것을 기대하는 class입니다.

여기서 우리가 주의 깊게 봐야 할 것은 removeRange가 왜 protected인가 인데요.
외부에서 clear를 호출해서 내부 데이터를 삭제하는데,

상속을 위 AbstractList를 상속한 구체 클래스에서

removeRange를 오버라이드하여 삭제 성능을 개선할 여지가 있기 때문에 p

rotected로 접근제한자를 의도적으로 열어둔 것이죠.

 

설명이 길었는데,

결론적으로 상속을 하면서 protected 메서드를 의도적으로 만들어 상위(부모)의 로직에서 성능개선을 하기 위해 열어둔 것처럼,

특정 코드에 끼워 넣어 사용할 수 있는 것을 hook이라고 합니다.

그래서 hook은 어떻게 설게하냐? → (우.테.코식 결론)'정답은 없다. 상황 봐가며 잘 예측해서 만들어라'라고 합니다.

 

 


 

둘째, 상속용 클래스의 생성자는 직접적으로든 간접적으로든 재정의 가능 메서드를 호출해서는 안된다.

이건 말보다 코드로 설명하면 명료합니다. 또 같이 코드를 보시죠.

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

    public void overrideMe() {
    }
}
public final class Child extends Parent {
    //초기화되지 않는 final 필드, 생성자에서 초기화한다.
    private final Instant instant;

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

    //재정의 가능 메서드, 상위 클래스의 생성자가 호출한다.
    @Override
    public void overrideMe() {
        System.out.println(instant);
    }

    public static void main(String[] args) {
        Child child = new Child();
        child.overrideMe();
    }
}

어떻게 될지 대충 감이 오시나요??

image

결과는 Child의 생성자에서 System.out.println으로 널을 한번 출력하고, 제대로된 instant를 출력합니다.

왜냐하면, Child의 생성자는 호출되면 가장 처음 super()를 통해 묵시적으로 parent를 생성자를 호출합니다.

Parent 생성자 내부에서 또 다시 overrideMe를 호출합니다.
Parent의 overrideMe는 재정의 되어 Child의 OverriedeMe를 호출하는데,
아직 생성이 다 되지 않아 null을 출력한 것이죠.


참고자료

[1] What is hook in java