-
[Effective Java] 아이템18: 상속보다는 컴포지션을 사용하라Language/Java 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'Language > Java' 카테고리의 다른 글
[Effective Java] 아이템20: 추상 클래스보다는 인터페이스를 우선하라 (0) 2022.07.09 [Effective Java] 아이템19: 상속을 고려해 설계하고 문서화하라. 그러지 않았다면 상속을 금지하라 (0) 2022.07.09 [Effective Java] 아이템17: 변경 가능성을 최소화하라 (0) 2022.06.29 [Effective Java] 아이템16: public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) 2022.06.24 [Effective Java] 아이템15: 클래스와 멤버의 접근 권한을 최소화하라 (0) 2022.06.22