Decorator
Decorator는 객체를 같은 인터페이스의 래퍼로 감싸서, 원본 코드를 건드리지 않고 기능을 덧붙이는 구조 패턴입니다. 감싸는 쪽도 감싸이는 쪽도 동일한 인터페이스를 구현하기 때문에, 클라이언트 입장에서는 원본을 쓰는지 데코레이터를 쓰는지 알 필요가 없습니다.
▶아키텍처 다이어그램
🔍 구조 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
기존 클래스에 기능 하나를 추가하고 싶을 때, 가장 직관적인 방법은 상속입니다. 그런데 기능 조합이 2개, 3개로 늘어나면 서브클래스 수가 기하급수적으로 불어납니다. 버퍼링+압축, 버퍼링+암호화, 압축+암호화, 버퍼링+압축+암호화... 조합마다 클래스를 만들면 코드가 폭발하고 유지보수는 사실상 불가능해집니다. 게다가 상속은 컴파일 타임에 고정됩니다. 런타임에 '이 요청에는 캐싱을 붙이고, 저 요청에는 빼자'라는 식의 동적 전환이 상속만으로는 어렵습니다.
GoF(Gang of Four)가 1994년에 이 패턴을 정리한 배경에는, 객체지향 프로그래밍이 널리 퍼지면서 상속의 한계가 실무에서 선명해진 시점이 있습니다. Smalltalk와 초기 Java에서 GUI 위젯이나 I/O 스트림을 설계할 때, 기능 조합의 경우의 수를 상속 계층으로 전부 표현하려다 클래스 트리가 감당할 수 없이 깊어지는 문제를 반복적으로 겪었습니다. Java의 `java.io` 패키지가 대표적인 사례입니다. `InputStream`을 `BufferedInputStream`, `DataInputStream`, `GZIPInputStream`으로 감싸는 구조는 상속이 아니라 위임과 합성으로 문제를 풀겠다는 Decorator 패턴의 직접적 적용입니다.
Decorator의 핵심은 '감싸기'입니다. 선물 포장에 비유하면, 상자 안에 물건(원본 컴포넌트)이 있고, 그 위에 포장지(데코레이터)를 겹겹이 씌울 수 있습니다. 포장지가 몇 겹이든 밖에서 보면 '상자'라는 점은 같습니다. 구조는 네 역할로 나뉩니다. 첫째, Component 인터페이스가 원본과 데코레이터가 공유하는 계약을 정의합니다. 둘째, ConcreteComponent가 실제 핵심 로직을 담은 원본 객체입니다. 셋째, BaseDecorator가 Component를 필드로 가지고 같은 인터페이스를 구현하며, 요청이 오면 내부 Component에 위임합니다. 넷째, ConcreteDecorator가 BaseDecorator를 상속해 위임 전후에 자기만의 로직을 끼워 넣습니다. 실행 흐름은 바깥 데코레이터부터 시작해 안쪽으로 위임하고, 결과가 다시 바깥으로 돌아오면서 각 계층의 추가 처리를 거칩니다.
Decorator와 Proxy는 둘 다 다른 객체를 감싸서 같은 인터페이스를 노출한다는 구조적 공통점이 있습니다. 차이는 의도에 있습니다. Decorator는 기능을 추가하기 위해 감싸고, Proxy는 접근을 제어하기 위해 감쌉니다. Decorator는 여러 겹 중첩해서 기능 조합을 만드는 것이 일반적이지만, Proxy는 보통 한 겹으로 원본 객체의 생성 시점, 권한 검사, 캐싱 같은 횡단 관심사를 처리합니다. Adapter와도 혼동될 수 있는데, Adapter는 호환되지 않는 인터페이스를 맞추는 것이 목적이라 내부 객체와 외부 인터페이스가 다릅니다. Decorator는 같은 인터페이스를 유지한 채 동작만 확장합니다. 기존 객체의 인터페이스를 바꿔야 하면 Adapter, 인터페이스는 유지하면서 동작을 늘려야 하면 Decorator입니다.
자주 비교하는 개념
Proxy
실제 객체에 대한 접근을 제어하는 대리 객체 패턴
둘 다 객체를 감싸서 같은 인터페이스를 노출하지만, Decorator는 기능을 추가하고 Proxy는 접근을 제어합니다.
Adapter
호환되지 않는 인터페이스를 연결하는 구조 패턴
Adapter는 호환되지 않는 인터페이스를 변환하고, Decorator는 같은 인터페이스를 유지하면서 동작을 확장합니다.
Composite
개별 객체와 복합 객체를 동일하게 다루는 트리 구조 패턴
Decorator는 단일 객체를 감싸서 기능을 추가하고, Composite는 여러 객체를 트리로 묶어 하나처럼 다룹니다. 둘 다 재귀적 합성을 쓰지만 해결하는 문제가 다릅니다.
Decorator는 기능 조합이 많고 조합마다 서브클래스를 만들기 어려운 상황에서 자연스럽게 등장합니다. Java I/O 스트림이 가장 널리 알려진 적용 사례입니다. `new BufferedReader(new InputStreamReader(new FileInputStream(file)))` 같은 코드가 바로 데코레이터 중첩입니다. 웹 프레임워크의 미들웨어 체인도 같은 원리입니다. 요청이 들어오면 인증 미들웨어, 로깅 미들웨어, 압축 미들웨어를 순서대로 통과하면서 각 단계가 요청을 가공하거나 검사합니다. 각 미들웨어는 독립적으로 추가·제거할 수 있고, 순서를 바꾸면 동작도 달라집니다. 도입 신호는 명확합니다. 기능 조합의 가짓수가 늘어나면서 상속 계층이 복잡해지기 시작할 때, 런타임에 특정 기능을 붙이거나 빼야 할 때, 기존 코드를 건드리지 않고 행동을 확장해야 할 때가 Decorator를 고려할 시점입니다. 다만 데코레이터가 너무 많이 중첩되면 디버깅 시 호출 스택이 길어지고 어느 계층에서 문제가 생겼는지 추적이 어려워질 수 있으므로, 조합 수가 관리 가능한 범위인지 확인하는 것이 좋습니다.