새 클래스의 인스턴스 메서드들은 기존 클래스에 대응하는 메서드를 호출해 그 결과를 반환한다.
기존 클래스의 구현이 바뀌거나, 새로운 메서드가 생기더라도 아무런 영향을 받지 않는다.
예시
변경 전
getAddCount() 실행 시 3이 출력될 줄 알았으나 6이 출력된다.
상위 클래스의 addAll() 구현 로직 상 add()를 호출하기 때문이다.
super.addAll();의 내부 구현을 알아야 코드를 짤 수 있다.
캡슐화가 깨지게 되는 것이다.
현재 클래스 로직 상에서는 무언가 더해지는 경우에 addCount를 증가하고 있는데, 만약 상위 클래스에 더하는 기능이 추가된다면?
하위 클래스에서 상위 클래스의 변경을 알지 못한다.
마찬가지로 상위 클래스의 구현을 알아야 해당 로직이 작동하게 코드를 짤 수 있게 된다.
상위 클래스에 의해 코드가 깨지게 될만한 여지가 꽤 많아진다.
// 코드 18-1 잘못된 예 - 상속을 잘못 사용했다! (114쪽)
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;
}
public static void main(String[] args) {
InstrumentedHashSet<String> s = new InstrumentedHashSet<>();
s.addAll(List.of("틱", "탁탁", "펑"));
// 3이 아니라 6이 출력된다.
System.out.println(s.getAddCount());
}
}
변경 후
상속이 아닌 Composition을 사용한다.
상속할 클래스를 멤버 변수로 정의한다.
Forwarding class, wrapper class라고도 부를 수 있는 ForwardingSet을 이용한다.
ForwardingSet도 마찬가지로 멤버 변수로 Set<E>을 사용하는 것을 확인할 수 있다.
InstrumentedSet에서 위의 예시와 동일한 연산 수행 시 3을 출력하게 된다.
기존에는 오버라이드한 addAll() 호출 시에 오버라이드한 add()가 호출되었다.
변경 후에는 addAll() 호출 시 side effect가 없다.
Wrapper class의 멤버 변수의 addAll() 구현이 어떻게 변경되더라도 해당 기능의 문제가 발생하지 않는다.
캡슐화가 잘 지켜진다.
상속하는 클래스가 변경되더라도, 현재는 인터페이스를 구현하고 있기 때문에 문제가 되지 않는다.
인터페이스가 변경된다면, ForwardingSet이 깨지게 되어 변경을 인지할 수 있게 된다.3
// 코드 18-3 재사용할 수 있는 전달 클래스 (118쪽)
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(); }
}
// 코드 18-2 래퍼 클래스 - 상속 대신 컴포지션을 사용했다. (117-118쪽)
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;
}
public static void main(String[] args) {
InstrumentedSet<String> s = new InstrumentedSet<>(new HashSet<>());
s.addAll(List.of("틱", "탁탁", "펑"));
System.out.println(s.getAddCount());
}
}
완벽 공략
p119, 데코레이터 패턴
p119, 컴포지션과 전달 조합은 넓은 의미로 위임(delegation)이라고 부른다.
상세하게 나눈다면 Delegation, Composition and Aggregation 등이 있겠으나
Delegation : A does it for B
Composition : A owns B
Aggregation : B is part of A
p119, 콜백 프레임워크와 셀프 문제
완벽 공략 34. 데코레이터(Decorator) 패턴
상속이 아닌 위임을 사용해서 보다 유연하게 런타임에 부가 기능을 추가하는 것도 가능하다.
기존의 빨강 집의 빨강 문을 파란 문으로 변경하려면
상속을 사용한다면, 빨강 지붕을 가지고, 빨강 벽을 가지고, 파란 문을 가진 클래스를 따로 만들어줘야한다.
매번 한 가지 요소가 변경될 때마다 클래스를 추가해줘야한다.
3 가지 요소라고 가정한다면 27개의 클래스
위임을 사용해 각 부분을 데코레이션으로 생각해 변경하도록 하면 위와같은 문제가 해결된다.
예시
Component: Set interface
Concrete component: HashSet
Decorator: ForwardingSet
Concrete decorator: InstrumentedSet
완벽 공략 35. 콜백 프레임워크와 셀프 문제
콜백 함수 : 다른 함수(A)의 인자로 전달된 함수(B)로, 해당 함수(A) 내부에서 필요한 시점에 호출될 수 있는 함수(B)를 말한다.
래퍼로 감싸고 있는 내부 객체가 어떤 클래스(A)의 콜백으로(B) 사용되는 경우에 this를 전달하면, 해당 클래스(A)는 래퍼가 아닌 내부 객체를 호출한다.
Self 문제
예시
아래와 같은 경우에는 의도한 대로 정상 작동하는 것을 확인할 수 있다.
class BobFunction implements FunctionToCall {
private final Service service;
public BobFunction(Service service) {
this.service = service;
}
@Override
public void call() {
System.out.println("밥을 먹을까..");
}
@Override
public void run() {
this.service.run(this);
}
}
public class Service {
public void run(FunctionToCall functionToCall) {
System.out.println("뭐 좀 하다가...");
functionToCall.call();
}
public static void main(String[] args) {
Service service = new Service();
BobFunction bobFunction = new BobFunction(service);
// 뭐 좀 하다가...
// 밥을 먹을까..
bobFunctionWrapper.run();
}
}
그렇다면 이제 wrapper를 사용해보자
public class BobFunctionWrapper implements FunctionToCall {
private final BobFunction bobFunction;
public BobFunctionWrapper(BobFunction bobFunction) {
this.bobFunction = bobFunction;
}
@Override
public void call() {
this.bobFunction.call();
System.out.println("커피도 마실까...");
}
@Override
public void run() {
this.bobFunction.run();
}
}
직관적으로 기대하는 바와 다르게 커피를 마시지 않는다.
Wrapper를 쓰는 이유가 없어진다.
BobFunctionWrapper의 run()을 살펴보면
bobFunction.run()을 호출하고 있다.
bobFunction.run()에서는 Wrapper로 감싸고 있는 객체인 self가 호출된다.
전달된 객체는 bobFunction이기 때문에 call()호출 시에 bobFunction의 call()만 호출된다.
this는 자기 자신을 감싸는 인스턴스를 반환하지 않는다.
public class Service {
public void run(FunctionToCall functionToCall) {
System.out.println("뭐 좀 하다가...");
functionToCall.call();
}
public static void main(String[] args) {
Service service = new Service();
BobFunction bobFunction = new BobFunction(service);
BobFunctionWrapper bobFunctionWrapper = new BobFunctionWrapper(bobFunction);
// 뭐 좀 하다가...
// 밥을 먹을까..
bobFunctionWrapper.run();
}
}
아이템 18. 상속보다는 컴포지션을 사용하라
핵심정리
예시
변경 전
getAddCount()
실행 시 3이 출력될 줄 알았으나 6이 출력된다.addAll()
구현 로직 상add()
를 호출하기 때문이다.super.addAll();
의 내부 구현을 알아야 코드를 짤 수 있다.변경 후
ForwardingSet
을 이용한다.ForwardingSet
도 마찬가지로 멤버 변수로Set<E>
을 사용하는 것을 확인할 수 있다.InstrumentedSet
에서 위의 예시와 동일한 연산 수행 시 3을 출력하게 된다.addAll()
호출 시에 오버라이드한add()
가 호출되었다.addAll()
호출 시 side effect가 없다.ForwardingSet
이 깨지게 되어 변경을 인지할 수 있게 된다.3완벽 공략
완벽 공략 34. 데코레이터(Decorator) 패턴
Component
: Set interfaceConcrete component
: HashSetDecorator
: ForwardingSetConcrete decorator
: InstrumentedSet완벽 공략 35. 콜백 프레임워크와 셀프 문제
예시