상속은 코드를 재사용하는 강력한 수단이지만 항상 최선의 방법은 아니다. 잘못 사용하게 되면 오류를 범하기 쉽다. 여기에서 말하는 상속은 “클래스가 다른 클래스를 확장(extends)하는” 구현 상속을 말한다. 클래스가 인터페이스를 구현하거나 또는 인터페이스가 또 다른 인터페이스를 확장하는 상속과는 무관하다.

상속을 이용하면 캡슐화를 깨뜨린다. 하위 클래스는 상위 클래스를 확장 했기 때문에 상위 클래스의 구현에 의존적일 수 밖에 없다. 따라서 상위 클래스 변화에 따라 하위 클래스도 발맞춰 변경 해야 한다. 그리고 다음 릴리스에서 상위 클래스에 새로운 메서드가 추가 되면, 하위 클래스는 추가된 메서드 호출에 따른 로직이 변경된 부분을 파악 해야 한다.

상속을 이용했을 떄의 문제점을 나타낸 예시를 보자.

public class InstrumentedHashSet<E> extends HashSet<E> {
    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(Arrays.asList("1", "2", "3"));

        System.out.println("this count is : " + s.getAddCount()); 
        System.out.println(s);
    }

    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 this.addCount;
    }
}

처음엔 위 예제의 결과가 3일거라 생각했지만, 정답은 6이다.

this count is : 6

addAll() 메서드는 내부적으로 add() 메서드를 이용한다는 이유이다.
즉, 처음 addCount += c.size() 로 인하여 3이 더해지고, 상위 클래스의 addAll() 메서드가 내부적으로 add() 메서드를 수행하여 addCount++ 가 3번 호출되어 총 6이 된 것.
따라서 실제 InstrumentedHashSet 사이즈는 3이지만, count변수는 6이 된다.

이러한 문제를 해결하기 위해 Composition을 사용하자

컴포지션을 이용하여 기존 클래스의 인스턴스를 private 필드로 참조하게 하는 방법을 사용할 수 있다.

  • 전달
    • forwarding
    • 새로운 클래스의 인스턴스는 기존 클래스의 대응하는 메서드를 호출하여 그 결과를 반환하는 방식
  • 전달 메서드
    • forwarding method
    • 새 클래스의 메서드들을 전달 메서드

이러한 방법은 새로운 클래스가 기존 클래스의 내부 구현 방식의 영향에서 벗어날 수 있도록 해준다.

컴포지션을 사용한 예제를 보자

// 래퍼클래스 
public class InstrumentedSet<E> extends ForwardingSet<E> {
    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<>(new TreeSet<>());
        s.add("1");
        s.add("2");
        s.add("3");
        s.addAll(Arrays.asList("4", "5", "6"));
        System.out.println("this count is : " + s.getAddCount());
    }
    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;
    }
}
// 전달 클래스
public class ForwardingSet<E> implements Set<E> {
    // 핵심부분
    // 임의의 set에 계측기능을 덧씌워 새로운 Set으로 만드는 것
    private final Set<E> s;

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

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

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

    public boolean isEmpty() {
        return s.isEmpty();
    }

    public int size() {
        return s.size();
    }

    public Iterator<E> iterator() {
        return s.iterator();
    }

    public boolean add(E e) {
        return s.add(e);
    }

    public boolean remove(Object o) {
        return s.remove(o);
    }

    public boolean containsAll(Collection<?> c) {
        return s.containsAll(c);
    }

    public boolean addAll(Collection<? extends E> c) {
        return s.addAll(c);
    }

    public boolean removeAll(Collection<?> c) {
        return s.removeAll(c);
    }

    public boolean retainAll(Collection<?> c) {
        return s.retainAll(c);
    }

    public Object[] toArray() {
        return s.toArray();
    }

    public <T> T[] toArray(T[] a) {
        return s.toArray(a);
    }

    @Override
    public boolean equals(Object obj) {
        return s.equals(obj);
    }

    @Override
    public int hashCode() {
        return s.hashCode();
    }

    @Override
    public String toString() {
        return s.toString();
    }
}
this count is : 6
  • 전달클래스
    • Set 인터페이스를 구현하여 Set의 기능들을 정의
    • 컴포지션을 이용하여 Set의 구현체를 인스턴스로 참조하도록 private 필드를 정의
    • 이 클래스는 Set의 기능들을 이용할 수 있도록 기능들을 전달하는 역할을 하고, Set에 또 다른 기능을 추가하여 새로운 Set을 구현할 수 있도록 해줌
  • 래퍼클래스
    • 전달 클래스를 상속하여 확장
    • 이러한 구조는 기존 클래스에 새로운 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 부름
    • 단 한가지 단점은?
      • 콜백 프레임워크에서의 래퍼가 아닌 내부 객체를 호출하는 self 문제
      • 이는 성능, 메모리 사용량에 영향을 끼칠까 걱정할 수 있지만, 실전에서는 별다른 영향이 없다고 밝혀졌다
      • 따라서 컴포지션 방법은 상속의 문제를 해결할 수 있는 좋은 방법

상속을 할 때는 반드시 하위 클래스가 상위 클래스와 is-a 관계인지 생각해봐야 한다. 만약 그렇지 않다면 상속 대신 컴포지션과 전달 메서드를 이용하는 편이 좋다.

댓글남기기