Replies: 1 comment
-
|
저는 데코레이터 패턴의 본질을 핵심은 데코레이터가 자신이 감싸는 대상과 같은 타입이라는 점인데, 이전 상속 토론(LSP)에서 정리했던 "행동 호환성"이 여기서 그대로 전제 조건이 됩니다. 1. 왜 상속만으로는 부족한가 — 조합 폭발발제에서 짚은 것처럼, 상속으로 기능 조합을 표현하면 조합의 수만큼 클래스가 필요합니다. 옵션이 우유, 샷, 휘핑, 시럽 4개라면 가능한 조합은 2⁴ = 16개입니다. 옵션이 하나 더 늘면 32개가 됩니다. 이걸 클래스로 표현하면 **조합 폭발(combinatorial explosion)**이 일어납니다. 저는 이 문제의 근본 원인이 상속은 컴파일 타임에 확장이 고정된다는 점에 있다고 생각합니다. 게다가 새 옵션(예: 카라멜)이 추가되면 기존의 모든 조합 클래스에 카라멜 버전이 추가로 필요해집니다. 이건 OCP(개방-폐쇄 원칙) 위반입니다. 확장할 때마다 기존 구조를 건드려야 하니까요. 데코레이터는 이 문제를 조합을 클래스가 아니라 객체 합성으로 표현해서 해결합니다. 옵션 하나당 데코레이터 하나만 만들면, 그것들을 런타임에 자유롭게 조합할 수 있습니다. 2. 데코레이터 패턴의 구조데코레이터는 네 가지 역할로 구성됩니다. // 1. Component: 공통 타입 (감싸는 것과 감싸이는 것이 공유)
public interface Coffee {
int price();
String description();
}
// 2. ConcreteComponent: 기본 구현
public class Americano implements Coffee {
public int price() { return 4000; }
public String description() { return "아메리카노"; }
}
// 3. Decorator: Component를 구현하면서, Component를 필드로 가짐 (핵심)
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee; // 감싸는 대상
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
// 4. ConcreteDecorator: 실제 기능 추가
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public int price() {
return coffee.price() + 500; // 위임 + 추가
}
public String description() {
return coffee.description() + " + 우유";
}
}구조의 핵심 두 가지 저는 이 구조에서 두 가지가 결정적이라고 생각합니다. (a) 데코레이터는 자신이 감싸는 것과 같은 타입이다 ( 이게 가장 중요합니다. (b) 데코레이터는 같은 타입의 객체를 필드로 가진다 ( 데코레이터가 3. 실제로 어떻게 조합되는가구조의 진가는 사용할 때 드러납니다. // 아메리카노 + 우유 + 샷 + 시럽
Coffee coffee = new SyrupDecorator(
new ShotDecorator(
new MilkDecorator(
new Americano())));
coffee.price(); // 4000 + 500 + 500 + 300 = 5300
coffee.description(); // "아메리카노 + 우유 + 샷 + 시럽"호출 흐름
마치 양파 껍질을 벗기듯, 각 데코레이터가 안쪽에 위임하고 자기 몫을 더합니다. 이전 토론에서 다룬 프록시(CGLIB)나 Filter 체인과 구조적으로 닮았다고 느꼈습니다. 셋 다 "같은 타입으로 감싸면서 전후에 무언가를 더한다"는 점이 공통입니다. 런타임 조합의 위력 Coffee 주문 = new Americano();
if (우유추가) 주문 = new MilkDecorator(주문);
if (샷추가) 주문 = new ShotDecorator(주문);조합이 코드 작성 시점이 아니라 런타임 사용자 선택에 따라 결정됩니다. 상속으로는 불가능한 부분입니다. 4. 상속 확장과의 비교 — 장점과 단점데코레이터의 장점
데코레이터의 단점
제 관점 저는 이 비교가 결국 이전 상속 토론과 이어진다고 생각합니다. 그 토론에서 "상속은 강한 결합을 만들고, 행동 호환이 안 되면 위험하다"고 정리했는데, 데코레이터는 상속 대신 합성을 써서 그 결합을 느슨하게 만든 것입니다. "상속보다 합성을 선호하라(Favor composition over inheritance)"는 원칙의 구체적 실현이라고 봅니다. 5. 데코레이터가 성립하기 위한 전제 — LSP저는 데코레이터를 쓸 때 반드시 의식해야 할 것이 데코레이터가 원본의 행동 계약을 깨면 안 된다는 점이라고 생각합니다. 이전 LSP 토론과 직결됩니다.
// ❌ 행동 호환을 깨는 데코레이터
public class BrokenDecorator extends CoffeeDecorator {
public int price() {
throw new UnsupportedOperationException(); // 클라이언트 기대 파괴
}
}이건 Penguin이 Bird의 6. 실제로 어디에 쓰이는가데코레이터는 추상적인 패턴 같지만, 사실 우리가 매일 쓰는 곳에 이미 있습니다. (a) 자바 IO 스트림 — 가장 유명한 예 BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream("file.txt")));
(b) Spring의 HttpServletRequestWrapper 요청을 가공해야 할 때(예: 본문을 여러 번 읽기 위해 캐싱) 원본 요청을 래퍼로 감쌉니다. 이것도 데코레이터입니다. (c) 컬렉션 래퍼
(d) 횡단 관심사 (프록시와의 경계) 로깅, 캐싱 같은 부가 기능을 덧입힐 때 데코레이터를 쓸 수 있습니다. 다만 이건 프록시 패턴과 헷갈리기 쉬운데, 아래에서 구분하겠습니다. 7. 프록시 패턴과의 차이이전에 CGLIB·프록시를 다뤘기 때문에, 데코레이터와 프록시의 차이를 짚는 게 의미 있다고 생각합니다. 둘은 구조가 거의 똑같습니다(같은 타입으로 감싼다). 차이는 의도에 있습니다.
즉 데코레이터는 "기능을 더하려고 일부러 감싸는 것"이고, 프록시는 "접근을 통제하려고 몰래 감싸는 것"입니다. 구조가 같아도 왜 감싸는가가 다릅니다. 8. 미션·실무에서 언제 적용을 고민할까저는 데코레이터를 다음 신호가 보일 때 고민합니다.
반대로 적용을 자제하는 경우도 분명합니다.
이번 예약 대기 미션을 예로 들면, 솔직히 데코레이터가 꼭 필요한 강한 동기는 적다고 생각합니다. 다만 "예약에 부가 정보를 단계적으로 덧붙이는 응답 가공"이나 "알림 채널(메일+푸시+SMS)을 조합하는" 기능이 생긴다면, 각 채널을 데코레이터로 만들어 조합하는 방식을 고려해볼 수 있다고 봅니다. 저는 패턴을 "쓸 수 있으니 쓰자"가 아니라, 조합 폭발이나 OCP 위반이라는 구체적 고통이 보일 때 꺼내는 도구로 여기는 편이 건강하다고 생각합니다. 9. 정리발제한 입장에서 제 생각을 정리하면 이렇습니다.
결론적으로 저는 데코레이터를 "상속보다 합성을 선호하라"는 원칙이 조합 확장이라는 구체적 문제에서 구현된 형태라고 봅니다. 그래서 단순히 "기능을 덧붙이는 기법"으로 외우기보다, 같은 타입을 유지하며 책임을 동적으로 쌓는 구조라는 본질로 이해하고, 조합 폭발이나 OCP 위반이라는 실제 고통이 보일 때 꺼내 드는 것이 적절하다고 생각합니다. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
객체에 기능을 추가해야 할 때 보통 상속을 먼저 떠올릴 수 있습니다.
하지만 기능 조합이 많아질수록 상속만으로 해결하기 어려운 상황이 생깁니다.
예를 들어 커피 주문 기능이 있다고 가정해보겠습니다.
기본 커피에 우유, 샷 추가, 휘핑, 시럽 같은 옵션을 붙일 수 있다면,
각 조합마다 클래스를 만들 경우 클래스 수가 빠르게 늘어날 수 있습니다.
이런 식으로 기능 조합이 늘어날수록 상속 구조는 복잡해지고, 새로운 옵션이 추가될 때마다 기존 구조를 계속 수정해야 할 수 있습니다.
데코레이터 패턴은 이런 문제를 해결하기 위해, 기존 객체를 감싸는 객체를 만들어 기능을 동적으로 추가하는 방식입니다.
즉, 객체 자체를 수정하거나 상속 계층을 계속 늘리지 않고도,
필요한 기능을 하나씩 감싸면서 확장할 수 있습니다.
그렇다면 데코레이터 패턴은 정확히 어떤 구조를 가지고 있을까요?
또 상속을 통한 확장과 비교했을 때 어떤 장점과 단점이 있을까요?
그리고 실제 프로젝트에서는 어떤 상황에서 데코레이터 패턴을 적용해볼 수 있을까요?
Beta Was this translation helpful? Give feedback.
All reactions