클래스가 다른 클래스를 확장하는 구현 상속의 경우 확장할 목적으로 설계되었고 문서화도 잘 된 클래스(아이템 19)일 때는 안전하지만,

다른 패키지의 구체 클래스를 상속하는 것은 위험하다. (클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터 페이스 상속은 상관 없음)

 

 

상속을 사용하지 않는 이유 중 하나는, 메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 

예를 들어, 상위 클래스는 릴리스마다 내부 구현이 달라질 수 있으며 , 그 여파로 하위 클래스가 오동작할 수 있다.

 

아래와 같이 HashSet을 상속하여 InstrumentedHashSet을 구현했다고 하자.

package com.example.sypark9646.item18;

import java.util.Collection;
import java.util.HashSet;

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

 

이 때, InstrumentedHashSet의 addCount를 직접 접근한 값과 size를 가져오는 것에 대한 결과는 매우 다름을 알 수 있다.

package com.example.sypark9646.item18;

import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class InstrumentedHashSetTest {

    @DisplayName("HashSet addAll override test")
    @Test
    void testInstrumentedHashSetTest() {
        InstrumentedHashSet<String> instrumentedHashSet = new InstrumentedHashSet<>();
        instrumentedHashSet.addAll(List.of("1", "2", "3"));

        Assertions.assertEquals(3, instrumentedHashSet.size());
        Assertions.assertEquals(3, instrumentedHashSet.getAddCount()); // 6
    }
}

 

이러한 결과가 나온 이유는, size의 경우 아래와 같이 map의 사이즈를 그대로 가져오기 때문에 제대로 된 크기가 나오는 것이다.

getAddCount의 경우는 addAll과 add에서 각각 값을 더하고 있다.

그런데, InstrumentedHashSet에서 상속하고 있는 addAll은 add 메서드를 사용하여 구현되어 있기 때문에 중복으로 값을 더하게 된다.

 

이 문제를 피하기 위한 근본적인 방법은 기존 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참 조하는 `컴포지션` 방식이다. 새 클래스의 인스턴스 메서드들은 (private 필드로 참조하는) 기존 클래스의 대응하는 메서드를 호출 하여 그 결과를 반환(전달, forwarding)하기 때문에, 새로운 클래스는 기존 클래스의 내부 구현 방식의 영향에서 벗어나기 때문에 기존 클래스에 새로운 메서드가 추가되더라도 영향 받지 않는다. (ex. Guava는 모든 컬렉션 인터페이스용 전달 메서드를 전부 구현해뒀다)

그렇다면 위 InstrumentedHashSet을 컴포지션과 전달 방식으로 다시 구현해 보자.

package com.example.sypark9646.item18;

import java.util.Collection;
import java.util.Set;

public class InstrumentedHashSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public InstrumentedHashSet(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;
    }
}

 

여기서 ForwardingSet은 전달 메서드만으로 이뤄진 재사용 가능한 전달 클래스이다.

package com.example.sypark9646.item18;

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

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();
    }
}
    @DisplayName("HashSet addAll Composition test")
    @Test
    void testCompositionInstrumentedHashSet() {
        InstrumentedHashSet<String> compositionInstrumentedHashSet = new InstrumentedHashSet<>(new TreeSet<>());
        compositionInstrumentedHashSet.addAll(List.of("1", "2", "3"));

        Assertions.assertEquals(3, compositionInstrumentedHashSet.size());
        Assertions.assertEquals(3, compositionInstrumentedHashSet.getAddCount());
    }

위의 ForwardingSet의 addAll은 set을 사용하고 있는데, 디버깅 해 보면 아래와 같이 사용한 Set의 인스턴스(TreeSet)의 addAll을 사용하게 되고, 

addAll이 add 메서드를 사용하더라도 TreeSet의 add 메서드를 사용하게 된다. (ForwardingSet의 add가 사용되는 것이 아님)

따라서 중복으로 addCount에 더해질 일이 없다.

 

다른 Set 인스턴스를 감싸고(wrap) 있다는 뜻에서 InstrumentedSet 같은 클래스를 래퍼 클래스라 하며,

다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern)이라고 한다.

2021/02/27 - [책을 읽자/Design Patterns] - Decorator Pattern

 

Decorator Pattern

 

sysgongbu.tistory.com

컴포지션과 전달의 조합은 넓은 의미로 위임(delegation)이라고 부른다. (래퍼 객체가 내부 객체에 자기 자신의 참조를 넘기는 경우)

래퍼 클래스는 단점이 거의 없다. 단, 래퍼 클래스가 콜백(callback) 프레임워크와는 어울리지 않는다는 점만 주의하면 된다.

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

 

 

상속은 반드시 하위 클래스가 상위 클래스의 `진짜` 하위 타입인 상황에서만 쓰여야 한다. (is-a 관계+상위 클래스가 확장을 고려해 설계 되었을 때)
자바 플랫폼 라이브러리에서 스택은 벡터가 아니므로 Stack은 Vector를 확장해서 는 안됐으며, 마찬가지로 속성 목록도 해시테이블이 아니므로 Properties도 Hashtable을 확장해서는 안 됐다. 두 사례 모두 컴포지션을 사용했다면 더 좋았을 것이다.

 

 

아래는 java.util.Stack을 컴포지션을 이용하여 리팩토링 해 본 코드이다.

package com.example.sypark9646.item18;

import java.util.EmptyStackException;
import java.util.Vector;

public class CustomStack<E> extends ForwardingVector<E> {

    public CustomStack(Vector<E> vector) {
        super(vector);
    }

    public E push(E item) {
        addElement(item);

        return item;
    }

    public synchronized E pop() {
        E obj;
        int len = size();

        obj = peek();
        removeElementAt(len - 1);

        return obj;
    }

    public synchronized E peek() {
        int len = size();

        if (len == 0) {
            throw new EmptyStackException();
        }
        return elementAt(len - 1);
    }

    public boolean empty() {
        return size() == 0;
    }

    public synchronized int search(Object o) {
        int i = lastIndexOf(o);

        if (i >= 0) {
            return size() - i;
        }
        return -1;
    }

    // private static final long serialVersionUID = 1224463164541339165L;
}
package com.example.sypark9646.item18;

import java.util.Collection;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Spliterator;
import java.util.Vector;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

public class ForwardingVector<E> extends Vector<E> {

    private final Vector<E> vector;

    public ForwardingVector(Vector<E> vector) {
        this.vector = vector;
    }

    public synchronized void copyInto(Object[] anArray) {
        vector.copyInto(anArray);
    }

    public synchronized void trimToSize() {
        vector.trimToSize();
    }

    public synchronized void ensureCapacity(int minCapacity) {
        vector.ensureCapacity(minCapacity);
    }

    public synchronized void setSize(int newSize) {
        vector.setSize(newSize);
    }

    public synchronized int capacity() {
        return vector.capacity();
    }

    public synchronized int size() {
        return vector.size();
    }

    public synchronized boolean isEmpty() {
        return vector.isEmpty();
    }

    public Enumeration<E> elements() {
        return vector.elements();
    }

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

    public int indexOf(Object o) {
        return vector.indexOf(o);
    }

    public synchronized int indexOf(Object o, int index) {
        return vector.indexOf(o, index);
    }

    public synchronized int lastIndexOf(Object o) {
        return vector.lastIndexOf(o);
    }

    public synchronized int lastIndexOf(Object o, int index) {
        return vector.lastIndexOf(o, index);
    }

    public synchronized E elementAt(int index) {
        return vector.elementAt(index);
    }

    public synchronized E firstElement() {
        return vector.firstElement();
    }

    public synchronized E lastElement() {
        return vector.lastElement();
    }

    public synchronized void setElementAt(E obj, int index) {
        vector.setElementAt(obj, index);
    }

    public synchronized void removeElementAt(int index) {
        vector.removeElementAt(index);
    }

    public synchronized void insertElementAt(E obj, int index) {
        vector.insertElementAt(obj, index);
    }

    public synchronized void addElement(E obj) {
        vector.addElement(obj);
    }

    public synchronized boolean removeElement(Object obj) {
        return vector.removeElement(obj);
    }

    public synchronized void removeAllElements() {
        vector.removeAllElements();
    }

    public synchronized Object clone() {
        return vector.clone();
    }

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

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

    public synchronized E get(int index) {
        return vector.get(index);
    }

    public synchronized E set(int index, E element) {
        return vector.set(index, element);
    }

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

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

    public void add(int index, E element) {
        vector.insertElementAt(element, index);
    }

    public synchronized E remove(int index) {
        return vector.remove(index);
    }

    public void clear() {
        vector.removeAllElements();
    }

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

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

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

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

    public boolean removeIf(Predicate<? super E> filter) {
        return vector.removeIf(filter);
    }

    public synchronized boolean addAll(int index, Collection<? extends E> c) {
        return vector.addAll(index, c);
    }

    @Override
    public synchronized boolean equals(Object o) {
        return vector.equals(o);
    }

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

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

    public synchronized List<E> subList(int fromIndex, int toIndex) {
        return vector.subList(fromIndex, toIndex);
    }

    public synchronized ListIterator<E> listIterator(int index) {
        return vector.listIterator(index);
    }

    public synchronized ListIterator<E> listIterator() {
        return vector.listIterator();
    }

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

    public synchronized void forEach(Consumer<? super E> action) {
        vector.forEach(action);
    }

    public synchronized void replaceAll(UnaryOperator<E> operator) {
        vector.replaceAll(operator);
    }

    public synchronized void sort(Comparator<? super E> c) {
        vector.sort(c);
    }

    public Spliterator<E> spliterator() {
        return vector.spliterator();
    }
}

 

위 CustomStack을 테스트 해 본 결과는 아래와 같다.

package com.example.sypark9646.item18;

import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import java.util.EmptyStackException;
import java.util.Stack;
import java.util.Vector;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class CustomStackTest {

    @DisplayName("stack 동작 비교 테스트")
    @Test
    void testCustomStack() {
        CustomStack<String> customStack = new CustomStack<>(new Vector<>());
        Stack<String> stack = new Stack<>();

        customStack.push("1");
        customStack.push("2");
        customStack.push("3");

        stack.push("1");
        stack.push("2");
        stack.push("3");

        Assertions.assertAll(
            () -> Assertions.assertEquals(stack.search("3"), customStack.search("3")),
            () -> Assertions.assertEquals(stack.search("10"), customStack.search("10")),
            () -> Assertions.assertEquals(stack.peek(), customStack.peek()),
            () -> Assertions.assertEquals(stack.pop(), customStack.pop()),
            () -> Assertions.assertEquals(stack.pop(), customStack.pop()),
            () -> Assertions.assertEquals(stack.pop(), customStack.pop())
        );
    }

    @DisplayName("empty stack exception 테스트")
    @Test
    void testCustomStack_WhenEmptyStackCallsPeek_ThrowException() {
        CustomStack<String> customStack = new CustomStack<>(new Vector<>());

        Assertions.assertTrue(customStack.empty());
        assertThatThrownBy(customStack::peek).isInstanceOf(EmptyStackException.class);
    }
}

+ Recent posts