Java

[Java] 컴포지션을 활용한 코드의 재사용 방법 (Effective Java 3rd_Item18)

Icarus8050 2019. 8. 4. 01:38
반응형

 상속은 상위 클래스의 코드를 재사용할 수 있도록 해주지만 캡슐화를 깨뜨립니다. 즉, 상위 클래스가 어떻게 구현되느냐에 따라서 하위 클래스의 동작에 영향을 미칠 수 있습니다. 그렇기 때문에 상위 클래스의 설계자는 확장을 충분히 고려하고, 문서화를 해두어야 합니다.

 

 Effective Java 3rd 책의 예제 코드를 한 번 살펴보겠습니다.

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 this.addCount;
    }
}
    @Test
    public void 잘못된_예제_테스트() {
        InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
        s.addAll(Arrays.asList("하나", "둘", "셋"));

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

 위의 코드는 HashSet 을 확장하여 처음 생성된 이후로 원소가 몇 개가 추가되었는지 카운트하는 클래스입니다. 만약 위의 클래스에서 테스트 코드와 같이 String 타입의 원소를 3개 추가했다면 addCount는 몇이 될까요?? 저는 간단하게 1차원적으로 생각하여 3이 될거라 생각했습니다.

this count is : 6

 하지만 결과는 6을 출력하였습니다. 처음에는 뭐지? 싶었습니다. 그 이유에 대한 내용을 살펴보니 addAll() 메서드는 내부적으로 add() 메서드를 이용한다는 것이었습니다. 즉, 처음 addCount += c.size() 로 인하여 3이 더해지고, 상위 클래스의 addAll() 메서드가 내부적으로 add() 메서드를 수행하여 addCount++ 가 3번 호출되어 총 6이 된 것입니다.

    /**
     * {@inheritDoc}
     *
     * <p>This implementation iterates over the specified collection, and adds
     * each object returned by the iterator to this collection, in turn.
     *
     * <p>Note that this implementation will throw an
     * <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is
     * overridden (assuming the specified collection is non-empty).
     *
     * @throws UnsupportedOperationException {@inheritDoc}
     * @throws ClassCastException            {@inheritDoc}
     * @throws NullPointerException          {@inheritDoc}
     * @throws IllegalArgumentException      {@inheritDoc}
     * @throws IllegalStateException         {@inheritDoc}
     *
     * @see #add(Object)
     */
    public boolean addAll(Collection<? extends E> c) {
        boolean modified = false;
        for (E e : c)
            if (add(e))
                modified = true;
        return modified;
    }

  HastSet의 추상하 클래스인 AbstractCollection 클래스에서 동작하는 addAll() 클래스입니다. addAll()을 보시면 반복문을 통해 add() 메서드가 호출되는 것을 확인할 수 있습니다.

 

 


 

 이러한 문제를 피해가기 위해서 기존 클래스를 상속하는 대신, 컴포지션을 이용하여 기존 클래스의 인스턴스를 private 필드로 참조하게 하는 방법을 사용할 수 있습니다. 새로운 클래스의 인스턴스는 기존 클래스의 대응하는 메서드를 호출하여 그 결과를 반환하는 방식을 전달(forwarding) 이라 하며, 새 클래스의 메서드들을 전달 메서드(forwarding method) 라고 합니다. 이러한 방법은 새로운 클래스가 기존 클래스의 내부 구현 방식의 영향에서 벗어날 수 있도록 해줍니다.

 

 위에서 보았던 InstrumentedSet 클래스에서 컴포지션과 전달(forwarding) 방식으로 다시 구현한 예제 코드를 보겠습니다.

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 obj) {
        return s.equals(obj);
    }

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

    @Override
    public String toString() {
        return s.toString();
    }
}

 우선 전달 클래스입니다. 전달 클래스는 Set 인터페이스를 구현하여 Set의 기능들을 정의하도록 하였고, 컴포지션을 이용하여 , Set의 구현체를 인스턴스로 참조하도록 private 필드를 정의하였습니다. 이 클래스는 Set의 기능들을 이용할 수 있도록 기능들을 전달하는 역할을 하고, Set에 또 다른 기능을 추가하여 새로운 Set을 구현할 수 있도록 해줍니다.

 

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;
    }
}
    @Test
    public void 컴포지션을_활용한_예제() {
        InstrumentedSet<String> s = new InstrumentedSet<>(new TreeSet<>());
        s.add("하나");
        s.add("둘");
        s.add("셋");
        s.addAll(Arrays.asList("넷", "다섯", "여섯"));
        System.out.println("this count is : " + s.getAddCount());
    }
this count is : 6

 위에서 보았던 전달 클래스를 상속하여 확장한 래퍼 클래스입니다. 이 클래스에는 아까와 같이 원소가 몇 개가 추가되는지 카운트 하는 기능을 가지고 있습니다.  이러한 구조는 기존 클래스에 새로운 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 부릅니다.

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

 

 

Decorator Pattern

참고용 데코레이터 패턴 구조

 


참고 자료

 

Effective Java 3rd (인사이트 출판)

반응형