(이펙티브 자바) 아이템 18. 상속보다는 컴포지션을 사용하라
상속으로 코드를 쉽게 재사용하지만, 이는 자칫 오류를 내기 쉬운 소프트웨어를 만들어낼 수 있다. 같은 프로그래머의 패키지나, 확장 목적의 클래스를 제외하고, 패키지를 넘어 다른 패키지의 구체 클래스를 상속하는 것은 위험하다.
(여기서 상속은 구현 상속을 말한다, 클리스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다)
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다. 상위클래스의 구현에 따라 그로 인해 하위 클래스 동작에 이상이 생길 수 있다.
추가한 원소 (현재 원소가 아닌)의 갯수를 확인하는 getAddCount 메서드를 가진 클래스이다.
package TestExtends;
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;
}
}
import TestExtends.InstrumentedHashset;
import java.util.Arrays;
import java.util.List;
public class test {
public static void main (String[] args) {
InstrumentedHashset<String> s = new InstrumentedHashset<>();
String[] str = {"틱", "탁", "펑"};
List<String> list = Arrays.asList(str);
s.addAll(list);
System.out.print(s.getAddCount()); // 3을 기대했지만, 6이 나옴을 확인!
}
}
이런 문제가 나타나는 이유는 addAll 메서드가 add를 활용하는데, addAll을 호출하면서, add를 한번씩 더 거치기 때문에 기대한 3이 아닌 6의 값이 호출된다. (addAll을 재정의하지 않으면 문제 해결은 가능, 그러나 addAll이 add메서드를 이용했음을 알고 있을 때 가능한 해법이라는 한계가 있다.)
상위 클래스인 HashSet의 매서드 동작을 다시 구현하는 것은 어렵기도 하고 시간도 더들고, 자칫 오류를 발생시키고, 성능을 떨어뜨릴 수 있다. 하위 클래스에서 접근할 수 없는 private 필드를 사용한다면 이 방식은 구현 자체가 불가능하다.
또한, 하위클래스에서 재정의하지 못한 새로운 메서드를 사용해 '허용되지 않은' 원소를 추가할 수도 있게 되는 문제도 생길 수 있다. (ex, Hashtable, Vector....)
이러한 문제를 해결할 수 있는 방법은 기존은 인터페이스를 활용하여 새로운 전달 클래스를 생성하는 것이다
package TestExtends;
import java.util.Collection;
import java.util.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;
}
}
package TestExtends;
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();}
}
이처럼 set 인터페이스를 재사용할 수 있는 전달클래스를 새로 만들면 사용함에 있어 견고하면서 유연한 설계가 가능하다. 이러한 설계를 "컴포지션"이라고 한다.
이때 다른 Set 인스턴스를 감싸고 있다는 의미에서 InstrumentedSet 같은 클래스를 래퍼 클래스라고 하며, 다른 Set에 계층 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다.
래퍼클래스의 단점이 거의 없지만, 콜백 프레임워크와 어울리지 않는 것만 주의하자.