Decorator
Decorator는 객체를 같은 인터페이스의 래퍼로 감싸서, 원본 코드를 건드리지 않고 기능을 덧붙이는 구조 패턴입니다. 감싸는 쪽도 감싸이는 쪽도 동일한 인터페이스를 구현하기 때문에, 클라이언트 입장에서는 원본을 쓰는지 데코레이터를 쓰는지 알 필요가 없습니다.
▶아키텍처 다이어그램
🔍 구조 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
기존 클래스에 기능 하나를 추가하고 싶을 때, 가장 직관적인 방법은 상속입니다. 그런데 기능 조합이 2개, 3개로 늘어나면 서브클래스 수가 기하급수적으로 불어납니다. 버퍼링+압축, 버퍼링+암호화, 압축+암호화, 버퍼링+압축+암호화... 조합마다 클래스를 만들면 코드가 폭발하고 유지보수는 사실상 불가능해집니다. 게다가 상속은 컴파일 타임에 고정됩니다. 런타임에 '이 요청에는 캐싱을 붙이고, 저 요청에는 빼자'라는 식의 동적 전환이 상속만으로는 어렵습니다.
기능 하나를 붙일 때마다 서브클래스를 하나씩 만드는 방식은 조합이 적을 때만 버틸 수 있습니다. 버퍼링, 압축, 암호화처럼 붙일 수 있는 기능이 둘셋만 늘어나도 클래스 수가 조합 수만큼 불어나고, 어떤 조합을 지원하는지조차 이름만 보고 파악하기 어려워집니다. GUI 위젯과 I/O 스트림을 다루던 초기 객체지향 라이브러리들이 바로 이 문제에 부딪혔습니다.
Decorator는 이런 압력에서 나온 패턴입니다. 상속으로 경우의 수를 늘리지 말고, 객체를 감싸는 래퍼를 조합해 기능을 쌓자는 접근입니다. Java의 java.io가 InputStream을 BufferedInputStream, DataInputStream, GZIPInputStream으로 겹겹이 감싸는 구조가 대표적인 예입니다.
Decorator의 핵심은 '감싸기'입니다. 선물 포장에 비유하면, 상자 안에 물건(원본 컴포넌트)이 있고, 그 위에 포장지(데코레이터)를 겹겹이 씌울 수 있습니다. 포장지가 몇 겹이든 밖에서 보면 '상자'라는 점은 같습니다. 구조는 네 역할로 나뉩니다. 첫째, Component 인터페이스가 원본과 데코레이터가 공유하는 계약을 정의합니다. 둘째, ConcreteComponent가 실제 핵심 로직을 담은 원본 객체입니다. 셋째, BaseDecorator가 Component를 필드로 가지고 같은 인터페이스를 구현하며, 요청이 오면 내부 Component에 위임합니다. 넷째, ConcreteDecorator가 BaseDecorator를 상속해 위임 전후에 자기만의 로직을 끼워 넣습니다. 실행 흐름은 바깥 데코레이터부터 시작해 안쪽으로 위임하고, 결과가 다시 바깥으로 돌아오면서 각 계층의 추가 처리를 거칩니다.
Decorator와 Proxy는 둘 다 다른 객체를 감싸서 같은 인터페이스를 노출한다는 구조적 공통점이 있습니다. 차이는 의도에 있습니다. Decorator는 기능을 추가하기 위해 감싸고, Proxy는 접근을 제어하기 위해 감쌉니다. Decorator는 여러 겹 중첩해서 기능 조합을 만드는 것이 일반적이지만, Proxy는 보통 한 겹으로 원본 객체의 생성 시점, 권한 검사, 캐싱 같은 횡단 관심사를 처리합니다. Adapter와도 혼동될 수 있는데, Adapter는 호환되지 않는 인터페이스를 맞추는 것이 목적이라 내부 객체와 외부 인터페이스가 다릅니다. Decorator는 같은 인터페이스를 유지한 채 동작만 확장합니다. 기존 객체의 인터페이스를 바꿔야 하면 Adapter, 인터페이스는 유지하면서 동작을 늘려야 하면 Decorator입니다.
Decorator는 '기능은 여러 개 붙어야 하는데, 조합마다 클래스를 만들기는 싫다'는 상황에서 힘을 발휘합니다. Java I/O 스트림이 대표적입니다. new BufferedReader(new InputStreamReader(new FileInputStream(file)))처럼 래퍼를 한 겹씩 둘러서 버퍼링과 문자 변환을 조합합니다.
웹 프레임워크의 미들웨어 체인도 같은 구조입니다. 요청이 들어오면 인증, 로깅, 압축 같은 관심사가 순서대로 요청을 감싸며 동작하고, 필요 없어진 단계는 중간에서 빼면 됩니다. 순서를 바꾸면 결과도 달라지므로, 기능을 독립적으로 유지하면서도 조합을 열어 둘 수 있습니다.
도입 신호도 분명합니다. 상속 계층이 기능 조합 수를 못 따라가기 시작할 때, 런타임에 특정 기능을 붙였다 떼야 할 때, 기존 코드를 수정하지 않고 행동을 확장해야 할 때가 Decorator를 고려할 시점입니다. 다만 래퍼가 너무 많이 중첩되면 호출 흐름 추적이 어려워지므로, 조합이 실제로 관리 가능한 수준인지 함께 봐야 합니다.