-
[Java] 컴포지션을 활용한 코드의 재사용 방법 (Effective Java 3rd_Item18)Java 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 (인사이트 출판)
반응형'Java' 카테고리의 다른 글
[Java] Image Resize 방법 (1) 2019.12.17 [Java] 제네릭과 가변인수 (Effective Java 3rd_item32) (0) 2019.08.18 [Java] 한정적 와일드 카드 (Effective Java 3rd_Item31) (0) 2019.08.11 [Java] Comparable 인터페이스 (0) 2019.07.29 [Java] Equals 의 재정의 규칙 (Effective Java 3rd_Item10) (0) 2019.07.25