스터디/이펙티브 자바

[Effective Java] Item18. 상속보다는 컴포지션

📝 작성 : 2022.05.29  ⏱ 수정 : 
728x90

여기서 말하는 상속은 다른 클래스를 확장하는 구현 상속입니다.

상속의 문제점

메서드 호출과 달리 상속은 캡슐화를 깨뜨립니다. 상위 클래스는 내부 구현이 달라질 수 있으며, 이로 인해 하위 클래스의 동작에 이상이 생길 수 있습니다.

또한, 자기사용(self-use) 여부에 따라 상속한 클래스에서 원하는 결과를 얻지 못 할 수도 있습니다. 또한 자기사용 여부는 다음 버전에서 유지될지 알 수 없습니다.

public class BadHashSet<E> extends HashSet<E> {
    //추가 된 원소 수
    private int addCount = 0;

    @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);
    }
}

BadHashSet<String> badHashSet = new BadHashSet<>();
badHashSet.addAll(List.of("1", "2"));
//-> addCount = 4
//super.addAll이 add 메서드를 사용하도록 구현되어 있기 때문에 각 원소를 추가할 때마다 addCount가 2씩 증가

해결법

private 필드로 기존 클래스의 인스턴스를 참조하는 컴포지션 설계를 합니다.
새 클래스의 인스턴스 메서드들은 기존 클래스의 메서드를 호출해서 그 결과를 반환합니다. 이 방식을 전달(forwarding)이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method)라 부릅니다.

// 래퍼 클래스
public class GoodSet<E> extends ForwardingSet<E> {
    private int addCount = 0;

    public GoodSet(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 class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;

    public ForwardingSet(Set<E> s) {
        this.s = s;
    }

    @Override
    public void clear() {
        s.clear();
    }

    @Override
    public boolean contains(Object o) {
        return s.contains(o);
    }

    @Override
    public boolean isEmpty() {
        return s.isEmpty();
    }
    //...
}

GoodSet은 구체적으로는 Set 인터페이스를 구현햏ㅆ고, Set의 인스턴스를 인수로 받는 생성자 하나를 제공합니다. 상속방식은 구체 클래스 각각을 따로 확장해야 하며, 지원하고자 하는 상위 클래스의 생성자 각각에 대응하는 생성자를 별도로 정의해야 합니다.

하지만 위의 컴포지션 방식은 어떤 Set 구현체라도 가능하며, 기본 생성자들과도 함께 사용할 수 있습니다.

 

다른 Set 인스턴스를 감싸고 있다는 뜻에서 GoodSet 같은 클래스를 래퍼 클래스라 하며, 다른 Set에 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 합니다.


래퍼 클래스는 콜백 프레임워크와 어울리지 않는다는 점만 주의하면 됩니다.

콜백 프레임 워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출(콜백) 때 사용하도록 합니다. 내부 객체는 래퍼의 존재를 모르니 대신 this를 넘기고, 콜백 때 래퍼가 아닌 내부 객체를 호출합니다. 이를 SELF문제라고 합니다.

 

정리

상속은 반드시 "진짜" 하위 타입인 상황에서만 사용해야 합니다. 순수하게 is-a 관계일 때만 상속해야하며 이때도 조심해서 상속해야 합니다. 하위 패키지의 클래스가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았을 수도 있기 때문입니다.

반응형