Item 18. 상속보다는 컴포지션을 사용하라

심규환·2022년 1월 25일
0

Effective Java

목록 보기
17/29
post-thumbnail

상속은 코드를 재사용하는 강력한 수단이지만 최선은 아니다. 잘못 사용한다면 오류를 낼 수 있게 된다. 상위 클래스, 하위 클래스 모두 같은 프로그래머의 통제 안에 있는 클래스라면 상속을 해도 상관없다. 하지만 패키지 밖에 있는 클래스를 상속하는 일은 위험하다.
이러한 상속은 캡슐화를 깨트린다.
만약 상위 클래스를 상속하여 하위 클래스를 구현한 뒤, 상위 클래스에서 릴리즈 때 다른 방식으로 구현하거나 새로운 것을 추가한다면 하위 클래스는 이를 추적하여 반영해야 한다. 만약 그렇지 않으면 갑작스런 오류가 날 수 있게 된다.

구체적인 예를 살펴보자.

public class InstrumentedhashSet<E> extends HashSet<E>{
    private int addCount = 0;
    public InstrumentedhashSet(){
    }
    
    public InstrumentedHashSet(int initCap, float loadFactor){
    	super(initCap, loadFactor);
    }
    
    @Override public boolean add(E e){
    	addCount++;
        return super.add(e);
    }
    
    @Override public boolean addAll(Collection<? extends E> c){
    	addCount += c.size();
        return super.addAll(c);
    }
    
    public int getAddCount(){
    	return addCount;
    }
}

자. 다음은 어떤 문제가 있을까? 만약 아래와 같이 컬렉션에 추가한다고 해보자.

InstrumentedhashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));

이제 getAddCount()를 하면 3이 나와야 할텐데. 실제 메소드를 사용해보면 6이 나온다. 이유가 뭘까?
바로 super.addAll(c)에서 문제가 발생한다. HashSet의 addAll은 하나의 요소마다 add를 사용한다. 그러면 addAll에서 한번에 3이 들어가고 add를 통해 1개씩 더 들어가게 된다.
이 경우 하위 클래스에서 addAll을 재정의하지 않으면 문제는 고쳐질 것이다. 하지만 이러한 count 증가 방식이 계속 유지되리란 보장이 없다.

또 하위 클래스가 깨지기 쉬운 이유가 있다. 다음 릴리스에서 상위 클래스에서 새로운 메서드를 추가하면 어떨까? 하위 클래스에서 보안 때문에 컬렉션에 추가된 모든 원소를 검사하는 코드로 재정의 했다고 해보자. 그런데 상위 클래스에서 값을 추가하는 메서드를 새로 추가한다면 하위 클래스의 컬렉션에 검증이 안된 값이 들어갈 수 있게 된다.

그러면 이 문제들을 피하는 묘안으로는 기존 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자. 상속이 아닌 원하는 클래스를 내부의 필드로 넣어 필요한 기능을 사용하라는 것이다.**(컴포지션 설계)**

다음은 컴포지션을 사용한 래퍼클래스이다.

public class InstrumentedSet<E> extends ForwardingSet<E>{
    private int addCount = 0;
    
    public InstrumentedSet(Set<E> s){
    	super(s);
    }
    
    @Override public boolean add(E e){
    	addCount++;
        return super.add(e);
    }
    
    @Override public boolean addAll(Collection<? extends E> c){
    	addCount += c.size();
        return super.addAll(c);
    }
    
    public int getAddCount(){
    	return addCount;
    }
}

위의 코드의 생성자를 보면 super 클래스를 그대로 생성하는 것을 볼 수 있다. InstrumentedSet은 생성하지 않고 그저 감싸주는 역할로 Set 기능을 사용하고 있다.

래퍼 클래스는 단점이 거의 없다. 한 가지 있다면 콜백 프레임워크와는 어울리지 않는다는 저만 주의하면 된다.

상속은 반드시 하위 클래스가 상위 클래스의 진짜 하위 타입인 상황에서만 쓰여야 한다. 가능하다면 컴포지션과 전달을 사용하도록 하자.

profile
장생농씬가?

0개의 댓글