컴포지션은 합성을 뜻한다.
상속은 코드를 재사용하는 강력한 수단이지만 항상 최선은 아니다 잘못 사용하면 오히려 오류를 내기 쉬운 소프트웨어를 만들게 된다
확장을 할 목적으로 설계하였고 문서도 잘 정리가 되어있으면 괜찮다
하지만 구체 클래스를 패키지를 경계를 넘어, 즉 다른 구체 클래스를 상속 받는것은 위험하다.
상속은 캡슐화를 깨뜨린다 다르게 말하면 상위 클래스가 어떻게 구현되느냐에 따라 하위 클래스 동작에 이상이 생긴다.
바로 코드 예를 보자면
직접 HashSet을 상속 받은 코드
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class InstrumentedHashSet<T> extends HashSet<T> {
private int addCount = 0;
public InstrumentedHashSet(int initCap, float loadFactor) {
super(initCap, loadFactor);
}
@Override
public boolean add(T e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends T> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
이렇게 직접 상속을 받아 구현한 객체를 실제 addAll 메서드를 호출해서 실제 addCount가 몇인지 확인해보겠다.
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(Arrays.asList("a", "b", "c"));
System.out.println("addCount = " + s.getAddCount());
자 이제 리스트에 원소를 3개를 집어넣었으니 addCount는 3이 나오길 기대하지만 실제로 값은 6이 나온다.
//출력결과
addCount = 6
어디서부터 잘못된것일까
HashSet의 addAll() 메서드는 add() 메서드를 사용해 구현된 데 있다.
add() 메서드에도 addCount++;
addAll() 메서드에도 addCount += c.size() 이렇게 코드를 작성하니 하위 클래스에서 전혀 예상치 못하게 한개의 원소마다 2개씩 카운트를 증가한셈이다.
바로 해결할 방법으로는 addAll() 메서드를 사용하지 않는것인데 근본적인 해결방법이 되지 않는다
왜냐하면 HashSet이 다음 릴리스에 어떻게 코드가 수정될지 모르는 상황에서 해당 클래스를 믿고 사용하기가 어렵고 위험이 도사린다.
또한 직접 HashSet 구현을 수정하려다가 내부 접근제어자가 private 인경우 직접적인 수정도 불가하다
그래서 우리가 주의깊게 살펴봐야할 것이 바로 컴포지션이다
기존 클래스의 대응하는 메서드를 호출해 그 결과를 반환한다.
이 방식을 전달(Forwarding) 이라 하며, 새 클래스의 메서드을 전달 메서드라 부른다.
그 결과 새로운 클래스는 기존 클래스 내부 구현방식의 영향에서 벗어나며, 심지어 기존 클래스에 새로운 메서드가 추가되도 전혀 영향받지 않는다.
래퍼 클래스 - 상속 대신 컴포지션 사용하기
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class InstrumentedHashSet<T> extends ForwardingSet<T> {
private int addCount = 0;
// 실제로 Set에게 위임하는 것이기에 변수명을 delegate로 작성했다.
public InstrumentedHashSet(Set<T> delegate) {
super(delegate);
}
@Override
public boolean add(T e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends T> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
}
재사용할 수 있는 클래스 전달
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
public class ForwardingSet<T> implements Set<T> {
private final Set<T> delegate;
public ForwardingSet(Set<T> delegate) {
this.delegate = delegate;
}
@Override
public int size() {
return delegate.size();
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
return delegate.contains(o);
}
@NotNull
@Override
public Iterator<T> iterator() {
return delegate.iterator();
}
@NotNull
@Override
public Object[] toArray() {
return delegate.toArray();
}
@NotNull
@Override
public <T1> T1[] toArray(@NotNull T1[] a) {
return delegate.toArray(a);
}
@Override
public boolean add(T t) {
return delegate.add(t);
}
@Override
public boolean remove(Object o) {
return delegate.remove(o);
}
@Override
public boolean containsAll(@NotNull Collection<?> c) {
return delegate.containsAll(c);
}
@Override
public boolean addAll(@NotNull Collection<? extends T> c) {
return delegate.addAll(c);
}
@Override
public boolean retainAll(@NotNull Collection<?> c) {
return delegate.retainAll(c);
}
@Override
public boolean removeAll(@NotNull Collection<?> c) {
return delegate.removeAll(c);
}
@Override
public void clear() {
delegate.clear();
}
}
이제 실제로 테스트를 해보자
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class HashSetMain {
public static void main(String[] args) {
HashSet<String> set = new HashSet<>();
InstrumentedHashSet<String> s = new InstrumentedHashSet<>(set);
s.addAll(Arrays.asList("a", "b", "c"));
System.out.println("addCount = " + s.getAddCount());
// 출력 결과는 3 우리가 기대한 값이 나왔다.
}
}
다른 Set 인스턴스를 감싸고 (wrap) 있다는 뜻에서 InstrumentedHashSet 같은 클래스를 래퍼 클래스라 하며, 다른 Set에 계측 기능을 덧씌운다는 뜻에서 데코레이터 패턴이라고 한다. * 유명한 데코레이터 패턴 예 (InputStream, outputStream)
상속은 강력하지만 캡슐화를 해친다는 문제가 있다. 상속은 상위 클래스와 하위 클래스가 순수하 is-a 관계일때만 써야 한다.
is-a 관계일 때도 안심할 순 없다.
하위 클래스의 패키지가 상위 클래스와 다르고, 상위 클래스가 확장을 고려해서 설계하지 않았다면, 여전히 문제가 된다
상속의 취약점을 피하기 위해선 우선 컴포지션과 전달을 사용하자. 특히 래퍼 클래스로 구현할 적당한 인터페이스가 있다면 더욱 그렇다.
래퍼 클래스는 하위 클래스보다 견고하고 강력하다.
'it 서적 독후감 > 이팩티브 자바' 카테고리의 다른 글
| [ITEM 17] 변경 가능성을 최소화 하라 (0) | 2025.03.13 |
|---|---|
| [ITEM 16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2025.03.11 |
| [ITEM 15] 클래스와 멤버의 접근 권한을 최소화 하라 (0) | 2025.03.10 |