Language/Java
[Effective Java] 아이템18: 상속보다는 컴포지션을 사용하라
wisdom11
2022. 7. 2. 04:45
이번 아이템에서 논하는 상속은 클래스가 다른 클래스를 확장하는 구현 상속
을 말한다.
클래스가 인터페이스를 구현하거나 인터페이스가 다른 인터페이스를 확장하는 인터페이스 상속과는 무관하다.
✔️ 상속의 위험성
메서드 호출과 달리 상속은 캡슐화를 깨뜨린다.
- 상위 클래스가 릴리스 때 내부 구현이 달라지면, 그 여파로 하위 클래스가 오동작할 수 있다.
- 메서드를 재정의하는 대신 새로운 메서드를 추가하더라도, 문제가 발생할 수 있다.
- 상위 클래스의 새 메서드와 시그니처가 같고 반환 타입이 다르다면, 컴파일 오류가 발생한다.
- 시그니처와 반환 타입 모두 같을 경우, 상위 클래스의 메서드를 재정의한 것이 된다.
- 새로 추가한 메서드가 상위 클래스의 메서드가 요구하는 규약을 만족하지 못할 가능성이 크다.
✔️ 컴포지션 (Composition)
이러한 문제를 해결하기 위한 방법은 바로 컴포지션
이다.
기존의 클래스를 확장하는 대신, 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하게 하자.
새 클래스의 인스턴스 메서드들은 기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
이 방식을 전달
(forwarding)이라고 하고, 새 클래스의 메서드들을 전달 메서드
(forwarding method)라고 한다.
컴포지션 방식을 사용하면 인스턴스도 계측할 수 있고, 기존 생성자들도 함께 사용할 수 있다.
예시 코드
// 코드 18-2 래퍼 클래스 - 상속 대신 컴포지션을 사용했다.
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;
}
}
// 코드 18-3 재사용할 수 있는 전달 클래스
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(); }
}
✔️ 컴포지션 단점
컴포지션의 단점은 거의 없다.
한 가지 주의할 점은 클래스가 콜백 프레임워크와는 어울리지 않는다는 점이다.
콜백 프레임워크에서는 자기 자신의 참조를 다른 객체에 넘겨서 다음 호출 때 사용하도록 한다.
내부 객체는 자신을 감싸고 있는 래퍼의 존재를 모르니 대신 자신의 참조를 넘기고, 콜백 때는 래퍼가 아닌 내부 객체를 호출하게 된다. (SELF 문제
)
핵심 정리
- 상속은 강력하지만 캡슐화를 해친다는 문제가 있다.
- 상속은 상위 클래스와 하위 클래스가 순수한 is-a 관계일 때만 사용해야 한다.
- is-a 관계일 때도 안심할 수만은 없는 것이, 하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해 설계되지 않았다.
- 상속의 취약점을 피하려면 상속 대신 컴포지션과 전달을 사용하자.
특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다.
래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
728x90