본문 바로가기

STUDY/이펙티브자바

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

상속

 

상속은 코드의 재사용성을 올려주고 유연한 개발을 가능하게 해주는 강력한 수단이다.  하지만 상속은 캡슐화를 깨뜨린다는 문제가 있다. 캡슐화는 클래스의 필드와 메소드의 동작방식을 외부에 숨기는 것을 말한다. 이를 통해 클래스와 외부 사용자는 서로에 대한 독립성을 가지고 서로에 대한 영향을 최소화할 수 있다. 하지만 상위 클래스를 상속받은 하위클래스는 상위클래스의 구현방식이 바뀌면 그에 따라 영향을 받는다.

 

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

    public static void main(String[] args) {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(List.of("Snap", "Crackle", "Pop"));
        System.out.println(s.getAddCount());
    }
}

// 3개의 문자열을 s에 넣었다.
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("하나", "둘", "셋));

위 코드는 HashSet를 상속하여 Set에 들어간 요소의 개수를 addCount 필드에 기록한다. 그래서 add, addAll 메소드가 호출될 때마다 addCount를 증가시켜준다. 하지만 이 코드를 실행해보면 기대와는 다른 결과가 나온다. 3개의 문자열을 넣은 s의 addCount는 6이 된다. 그 이유는 부모 클래스인 HashSet가 addAll 호출시 매 요소마다 add 메소드를 다시 호출하기 때문이다. 이처럼 클래스를 상속하면 부모 클래스의 내부 구현에 영향을 받게된다.

 

 


컴포지션

 

public class ForwardingSet<E> implements Set<E> {
    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 o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

 

컴포지션은 필요한 클래스를 상속받는 대신 필드 객체로 만들어서 사용하는 것을 말한다. ForwardingSet 클래스를 보면 Set 인터페이스를 구현하면서 내부 필드 객체로 Set 구현체를 가지고 있다. 그리고 Set 인터페이스의 구현을 모두 필드 객체인 s에 위임하고 있다.

 

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

    public static void main(String[] args) {
        InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
        s.addAll(List.of("Snap", "Crackle", "Pop"));
        System.out.println(s.getAddCount());
    }
}

// 3개의 문자열을 s에 넣었다.
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
s.addAll(List.of("하나", "둘", "셋));

이렇게 상속 대신 컴포지션을 사용한 ForwardingSet 클래스를 상속하여 InstrumentedSet 클래스를 만들었다. 이제는 기대한대로 addCount가 3으로 나오고, HashSet의 구현내용에 영향을 받지 않게 되었다.

728x90