같은 인터페이스로 객체를 감싸 기능을 덧붙이는 패턴.
interface DataSource { void write(String data); }
class FileDataSource implements DataSource { ... }
abstract class Decorator implements DataSource {
protected final DataSource wrappee;
Decorator(DataSource d) { this.wrappee = d; }
}
class Encryption extends Decorator {
public void write(String data) { wrappee.write(encrypt(data)); }
}
class Compression extends Decorator {
public void write(String data) { wrappee.write(compress(data)); }
}
// 런타임에 자유롭게 조합한다
DataSource ds = new Encryption(new Compression(new FileDataSource("a.txt")));
왜 상속이 아닌가 — 조합 폭발
기능 3개를 상속으로 조합하면
- 암호화 · 압축 · 로깅 · 암호화+압축 · 암호화+로깅 · 압축+로깅 · 셋 다
- 클래스 7개. 기능이 하나 늘면 15개
데코레이터는 클래스 3개로 모든 조합을 만든다
자바 IO가 교과서다
new BufferedReader(new InputStreamReader(new FileInputStream("a.txt")))
// 파일 읽기 + 문자 변환 + 버퍼링을 각각의 데코레이터로 쌓았다
순서가 의미를 바꾼다
-
Encryption(Compression(file)) — 압축한 뒤 암호화 → 압축률이 좋다
-
Compression(Encryption(file)) — 암호화한 뒤 압축 → 암호문은 압축이 거의 안 된다
-
같은 부품인데 순서로 결과가 달라진다 — 문서화가 필요한 지점이다
Proxy와 무엇이 다른가
-
형태는 거의 같다 (같은 인터페이스로 감싼다). 의도가 다르다
-
Decorator — 기능을 '더한다'. 여러 개를 쌓는 것이 전제다
-
Proxy — 접근을 '제어한다'. 대개 하나만 감싼다 (지연 로딩 · 권한 · 캐싱 · 원격)
남용의 경계
-
겹이 깊어지면 디버깅이 어렵다 (스택 트레이스가 길어진다)
-
어느 겹에서 문제가 생겼는지 추적이 힘들다
-
조합이 실제로 여러 가지 필요할 때만 값을 한다
-
조합이 늘 고정이라면 그냥 한 클래스가 낫다
면접 함정
- ❌ "Decorator는 상속의 대체" → 상속을 쓰긴 한다(같은 인터페이스를 구현). 핵심은 합성으로 조합한다는 점이다.
- ❌ "Proxy와 Decorator는 같다" → 구조가 같고 의도가 다르다.
스프링·자바에서 만나는 형태
// 컬렉션 래핑
Collections.unmodifiableList(list) // 수정 불가 데코레이터
Collections.synchronizedList(list) // 동기화 데코레이터
// HTTP 클라이언트 인터셉터 체인도 같은 발상이다
client.newBuilder()
.addInterceptor(new LoggingInterceptor())
.addInterceptor(new AuthInterceptor())
.build();
함수 합성으로도 표현된다
Function<String, String> pipeline =
((Function<String, String>) this::compress).andThen(this::encrypt);
데코레이터가 하는 일이 결국 "감싸서 앞뒤에 무언가를 더하는 것" 이라, 함수형에서는 고차 함수로 자연스럽게 표현된다.
인터페이스가 크면 부담이 된다
데코레이터는 감싸는 인터페이스의 모든 메서드를 구현해야 한다
→ 메서드가 20개면 데코레이터마다 20개를 위임해야 한다
→ 추상 데코레이터 클래스를 두어 기본 위임을 한 번만 쓴다
→ 코틀린은 by 키워드로 이 위임이 한 줄이다