목차
조합마다 하위 클래스를 만들면 폭발한다
알림을 보내는 EmailNotifier가 있다. 여기에 부가 기능을 붙이고 싶다. 보내기 전후로 로그를 남기고, 실패하면 재시도하고 싶다.
상속으로 하면 이렇게 된다.
class LoggingEmailNotifier extends EmailNotifier { ... }
class RetryEmailNotifier extends EmailNotifier { ... }
class LoggingRetryEmailNotifier extends EmailNotifier { ... } // 둘 다로그·재시도의 조합마다 클래스가 하나씩 생긴다. 게다가 SmsNotifier에도 같은 기능이 필요하면, LoggingSmsNotifier·RetrySmsNotifier… 알림 종류 × 기능 조합만큼 클래스가 폭발한다.
데코레이터 = 감싸서 기능을 덧입힌다
데코레이터 패턴이 이걸 푼다.
객체를 같은 모양의 껍질로 감싸서, 원래 동작 앞뒤에 새 기능을 덧입힌다. 껍질은 여러 겹 쌓을 수 있다.
로그 기능을 EmailNotifier에 상속으로 박는 게 아니라, 로그를 씌우는 껍질을 만들어 감싼다. 재시도도 또 다른 껍질이다. 필요한 껍질을 골라 겹치면 조합이 만들어진다. 클래스를 미리 다 만들 필요가 없다.
감싸는 것도 같은 인터페이스다
데코레이터의 핵심은 껍질이 알맹이와 같은 인터페이스라는 것이다.
public interface Notifier {
void send(String message);
}EmailNotifier도 Notifier이고, 그걸 감싸는 로그 껍질도 Notifier다. 그래서 감싸도 밖에서 보면 여전히 Notifier다. 한 겹 씌우든 세 겹 씌우든, 쓰는 쪽은 그냥 Notifier.send()를 부른다. 감싼 것과 안 감싼 것이 구별되지 않는다 - 이게 여러 겹 쌓기를 가능하게 한다.
데코레이터를 만든다
알맹이와 껍질의 공통 뼈대를 세운다.
public class EmailNotifier implements Notifier {
public void send(String message) { /* 이메일 전송 */ }
}
// 껍질의 공통 부모 - 감쌀 대상을 들고 그대로 위임한다
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
protected NotifierDecorator(Notifier wrapped) {
this.wrapped = wrapped;
}
public void send(String message) {
wrapped.send(message); // 기본은 그냥 넘긴다
}
}NotifierDecorator는 Notifier를 하나 들고, send()를 그대로 넘긴다. 여기에 각 껍질이 자기 기능을 얹는다.
기능을 앞뒤에 덧붙인다
구체 껍질들은 send()를 오버라이드해서, 위임 앞뒤에 기능을 끼운다.
public class LoggingDecorator extends NotifierDecorator {
public LoggingDecorator(Notifier wrapped) { super(wrapped); }
@Override
public void send(String message) {
log.info("보내기 전: {}", message);
super.send(message); // 감싼 대상에게 넘긴다
log.info("보낸 뒤");
}
}
public class RetryDecorator extends NotifierDecorator {
public RetryDecorator(Notifier wrapped) { super(wrapped); }
@Override
public void send(String message) {
for (int attempt = 1; attempt <= 3; attempt++) {
try {
super.send(message);
return;
} catch (Exception e) {
if (attempt == 3) throw e; // 마지막 시도도 실패하면 다시 던진다
}
}
}
}LoggingDecorator는 위임 앞뒤에 로그를 찍고, RetryDecorator는 위임을 재시도로 감싼다. 각자 자기 한 가지 기능만 얹고, 실제 전송은 감싼 대상에게 맡긴다.
여러 겹으로 쌓는다
이제 껍질을 골라 겹친다.
Notifier notifier =
new LoggingDecorator(
new RetryDecorator(
new EmailNotifier()));
notifier.send("주문 완료");바깥부터 안으로 로그 → 재시도 → 전송 순으로 흐른다. 재시도만 필요하면 로그 껍질을 빼고, SMS에 붙이고 싶으면 EmailNotifier 자리에 SmsNotifier를 넣으면 된다. 껍질과 알맹이를 런타임에 자유롭게 조립하는 것이다. 순서를 바꾸면 동작도 바뀐다(로그가 재시도 밖이냐 안이냐).
상속과 무엇이 다른가
상속으로도 기능을 더할 수 있는데 왜 데코레이터인가.
- 상속은 컴파일 타임에 고정된다.
LoggingRetryEmailNotifier는 만들 때 이미 정해진 조합이다. 조합마다 클래스를 미리 만들어야 한다. - 데코레이터는 런타임에 조립된다. 어떤 껍질을 어떤 순서로 씌울지 실행 중에 정한다. 조합의 수만큼 클래스를 만들 필요가 없다.
그래서 “기능 조합이 많고, 실행 중에 갈아 끼우고 싶을 때” 데코레이터가 상속을 이긴다.
어댑터와는 반대다
감싼다는 점에서 어댑터와 비슷해 보이지만, 목적이 정반대다.
- 어댑터는 모양(인터페이스)을 바꾼다. 하는 일은 그대로 두고, 안 맞던 인터페이스를 맞게 번역한다.
- 데코레이터는 모양을 그대로 둔다. 같은 인터페이스를 유지하면서, 하는 일에 기능을 더한다.
한쪽은 모양을 바꿔 붙게 하고, 한쪽은 모양을 지켜 겹쳐 쌓는다. 둘 다 감싸지만 향하는 곳이 다르다.
실무: java.io와 공통 관심사
데코레이터의 교과서적 실물이 **자바의 java.io**다.
Reader reader = new BufferedReader(new FileReader("a.txt"));BufferedReader가 FileReader를 감싸 버퍼링을 더한다. 둘 다 같은 Reader라, 감싸도 여전히 Reader다. 그래서 필요하면 이렇게 여러 겹으로 쌓을 수 있다. 각 껍질이 한 가지 기능을 더하면서 같은 인터페이스를 유지하는 것 - 딱 데코레이터다.
그 밖에도 로깅·캐싱·압축·암호화처럼 핵심 로직에 얹는 공통 관심사를 원본을 안 건드리고 씌울 때 쓴다. 필요한 곳에만, 필요한 만큼 겹치는 게 값어치다.
정리
| 데코레이터란 | 같은 인터페이스로 감싸 기능을 덧입히고, 여러 겹 쌓는다 |
| 문제 | 기능 조합마다 하위 클래스를 만들면 폭발한다 |
| 핵심 | 껍질이 알맹이와 같은 인터페이스 - 감싼 것과 안 감싼 것이 구별 안 됨 |
| 어떻게 | 위임 앞뒤에 기능을 끼우고, 실제 일은 감싼 대상에게 넘긴다 |
| 상속과 | 상속=컴파일 타임 고정, 데코레이터=런타임 조립 |
| 어댑터와 | 어댑터는 모양을 바꾸고, 데코레이터는 모양을 지켜 기능을 더한다 |
원본을 건드리지 않고, 같은 모양의 껍질로 감싸 기능을 얹는 것. 필요한 껍질을 골라 겹쳐 조합을 만드는 것 - 그게 데코레이터다.