티스토리 뷰

728x90
반응형

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


  • 여기서 말하는 상속은 클래스가 다른 클래스를 확장하는 상속을 말합니다. 그렇기 때문에 클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 상속과는 무관합니다.
  • 상속을 사용하게 되면 메서드 호출과 달리 캡슐화를 깨뜨립니다. 다르게 말하면 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스의 동작에 이상이 생길 수 있으며, 상위 클래스는 릴리즈마다 내부 구현이 달라질 수 있어서 그 여파로 하위 클래스가 오작동할 수 있습니다.
  • 컴포지션이란 다른 클래스에서 상위 클래스를 private 필드로 작성해 참조하도록 하여 기존 상위 클래스가 다른 클래스의 구성 요소로 쓰인다는 의미입니다.

 

🧨 문제가 발생하는 예제

  • 아래 예제를 실행 했을 때 결과는 3이 나올것 같지만 실제 결과는 6이 출력되게 됩니다.
  • HashSet 클래스의 addAll 메서드는 내부적으로 add 메서드를 호출하게 됩니다. 그렇기 때문에 addCount++이 호출되면서 6이 출력되게 되었습니다. 이런 내부 구현 방식은 HashSet 문서에도 당연히 없기 때문에 아래와 같은 문제는 addAll 메서드를 재정의 하지 않으면 고칠 수 있는 문제입니다. 
@Getter
public class InstrumentedHashSet<E> extends HashSet<E> {

    private int addCount = 0;

    public InstrumentedHashSet() {}

    public InstrumentedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, 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 class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        InstrumentedHashSet<String> set = new InstrumentedHashSet<>();
        set.addAll(List.of("hello", "java", "hard"));

        System.out.println(set.getAddCount()); // 6
    }
}

abstract class AbstractCollection

 

💡 컴포지션을 사용하자.

  • 위와 같은 문제들은 새로운 클래스에서 해당 클래스를 private 필드로 작성해 참조하도록 합니다. 이를 기존 클래스가 새로운 클래스의 구성요소로 쓰인다는 의미로 컴포지션이라고 합니다.
  • 새로운 클래스에서 기존 클래스에 대응하는 메서드를 호출하면 새로운 클래스는 기존 클래스의 메서드를 호출해서 결과를 반환하는데 이를 전달이라고 하며, 이런 새로운 클래스의 메서드를 전달 메서드라고 부릅니다.
@Getter
public class CompositionInstrumentedSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public CompositionInstrumentedSet(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 int size() { return 0; }

    @Override
    public boolean isEmpty() { return false; }

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

    @Override
    public Iterator<E> iterator() { return null; }

    @Override
    public Object[] toArray() { return new Object[0]; }

    @Override
    public <T> T[] toArray(T[] a) { return null; }

    @Override
    public boolean add(E e) { return false; }

    @Override
    public boolean remove(Object o) { return false; }

    @Override
    public boolean containsAll(Collection<?> c) { return false; }

    @Override
    public boolean addAll(Collection<? extends E> c) { return false; }

    @Override
    public boolean retainAll(Collection<?> c) { return false; }

    @Override
    public boolean removeAll(Collection<?> c) { return false; }

    @Override
    public void clear() { }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) throws Exception {
        CompositionInstrumentedSet<String> set = new CompositionInstrumentedSet<>(new HashSet<>());
        set.addAll(List.of("hello", "java", "hard"));
        System.out.println(set.getAddCount()); // 3
    }
}

 

 

💡 정리

  • 상속을 캡슐화를 깨트립니다.
  • 상속은 상위 클래스와 하위 클래스의 관계가 온전한 is-a 관계일 때만 사용해야 합니다.
  • is-a 관계라 할지라도 하위 클래스의 패키지가 상위 클래스의 패키지와 다르고, 상위 클래스가 확장을 고려하지 않은 경우에는 문제가 발생할 수 있습니다.

 

 

 

728x90
반응형