요약
•
상속은 두 클래스가 is-a 관계일 때(완전히 하위 타입일 때)만 사용하자.
•
그렇지 않다면 내부의 private 인스턴스로 클래스를 두고 구현하는 컴포지션을 사용하자.
상속 받은 메서드의 재정의 문제점
•
메서드 호출과는 달리 상속은 캡슐화를 깨뜨린다. 다시 말해, 상위 클래스의 구현에 따라 하위 클래스 동작에 이상이 생길 수 있다.
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {
}
@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);
}
}
Java
복사
•
위 코드는 잘 구현된 것처럼 보이지만, 실제로 제대로 동작하지 않는다. addAll 메서드로 원소를 3개 더하는 경우, 내부의 addCount 값은 3이 아니라 6이 저장되어 있다.
•
해당 문제의 원인은 HashSet의 addAll이 각 원소를 add 메서드 호출 하도록 구현되어 있는데, 각 메서드를 호출할 때 InstrumentedHashSet에서 재정의된 add 메서드가 호출되면서 addCount가 중복으로 값이 더해진 것이다.
•
이 부분은 add 메서드를 재정의 하지 않으면 제대로 동작하지만, 해당 방법은 HashSet이 add 메서드를 정의했다는 전제를 두고 구현된 해결책이라는 한계가 있다. 이렇게 자기사용(self-use)된 메서드를 적용하면 자바의 다음 릴리즈에서 HashSet이 수정될 지 여부를 알 수 없기 때문에 깨지기 쉽다.
•
위 코드에서 addAll이 부모의 메서드를 호출하는 것이 아니라, 컬렉션을 순회하며 add 메서드를 한번씩만 호출하는 방법은 조금 더 나은 방법이다. 하지만 상위 메서드의 동작을 다시 구현해야 하기 때문에, 어렵고 시간이 많이 들고 오류를 내거나 성능을 떨어뜨릴 수 있다는 단점이 있다. 또한 상위 클래스에 새로운 메서드가 추가된다면 역시 문제가 될 수 있다.
컴포지션으로 구현하기
•
위 문제는 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 만들어 해결할 수 있다. 이런 방법은 기존 클래스가 새로운 클래스의 구성요소로 사용된다는 뜻에서 컴포지션(composition)이라 한다.
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 lterator<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 retalnAll(Collectlon<?> c) { return s.retainAll(c); )
public Object[] toArray() { return s.toArray(); }
public <T> T[] toArray(T[] a) { return s.toArray(a); }
@0verride public boolean equals(Object o) { return s.equals(o); }
@0verride public int hashCode() { return s.hashCode(); }
@0verride public String toString() { return s.toString(); }
}
Java
복사
public class InstrumentedHashSet<E> extends ForwardingSet<E> {
private int addCount = 0;
public InstrumentedHashSet() {
}
@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);
}
}
Java
복사
•
이와 같이 HashSet의 모든 기능을 정의한 Set 인터페이스를 구현하여, 래퍼 클래스로 사용하면 견고하고 유연하게 대처할 수 있다. 이렇게 사용하는 방법을 덧씌운다는 뜻에서 데코레이터 패턴(Decorator pattern)이라고 한다.
•
이런 래퍼 클래스에서는 단점이 거의 없지만, 래퍼 클래스를 콜백(callback) 프레임워크에 사용되면 내부 객체는 깜사고 있는 래퍼 클래스의 존재를 모르니 자신(this)의 참조를 넘기고 콜백 때 래퍼 클래스가 아닌 내부 객체를 호출하게 되는 문제가 있다. 이를 SELF 문제라 부른다.
상속과 컴포지션
•
상속은 반드시 하위 클래스가 상위 클래스의 완전히 하위 타입인 상황에서만 사용되어야 한다. 다시 말해 클래스 A와 클래스 B가 is-a 관계일 때만 B가 A를 상속해야 한다.
•
그렇지 않다면 클래스 A를 private 인스턴스로 두고 내부 구현하는 컴포지션을 사용하자.