| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- 스프링
- 템플릿 메서드 패턴
- 스케줄러
- redis
- GoF 23
- Spring Batch
- 프로그래머스
- 로드밸런서
- 트러블슈팅
- 자바
- 계산기
- java
- 빌더 패턴
- 코드카타
- lv1
- 배치
- Spring
- Effective Java
- 프록시 패턴
- spring boot
- 백엔드
- 김영한
- 이펙티브 자바
- Til
- 스프링 배치
- 성능 개선
- DB
- 디자인 패턴
- 토스
- 추상클래스
Archives
- Today
- Total
김코딩
데코레이터 패턴(Decorator Pattern) 본문
디자인 패턴 시리즈의 일곱 번째 주제는 데코레이터 패턴(Decorator Pattern)이다.
어댑터 패턴이 "인터페이스를 변환"하는 거였다면, 데코레이터 패턴은 "기능을 추가"하는 것이다.
예를 들어, 커피숍에서 아메리카노를 주문한다고 생각해보자. 기본 아메리카노에 샷 추가, 휘핑크림 추가, 시럽 추가... 이렇게 옵션을 덧붙이면 가격도 올라가고 기능도 추가된다. 데코레이터 패턴은 이처럼 객체에 기능을 장식(Decorate)하듯이 추가하는 패턴이다.
이번 글에서는 데코레이터 패턴이 무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.
데코레이터 패턴의 정의
데코레이터 패턴(Decorator Pattern)은 객체에 추가적인 기능을 동적으로 첨가하는 패턴이다.
기존 코드를 수정하지 않고, 객체를 감싸서 새로운 기능을 추가할 수 있다.
핵심 특징:
- 객체에 동적으로 기능을 추가한다
- 기존 코드를 수정하지 않는다
- 기능을 조합할 수 있다
- 래퍼(Wrapper)처럼 객체를 감싼다
현실 비유:
- 커피에 토핑 추가하기
- 피자에 토핑 추가하기
- 선물 포장하기
왜 데코레이터 패턴이 필요할까?
커피숍 주문 시스템을 개발한다고 가정해보자.
방법 1: 상속 사용 (문제가 많음)
// 기본 커피
public class Coffee {
public int getCost() {
return 3000;
}
public String getDescription() {
return "아메리카노";
}
}
// 샷 추가 커피
public class CoffeeWithShot extends Coffee {
@Override
public int getCost() {
return 3000 + 500;
}
@Override
public String getDescription() {
return "아메리카노 + 샷";
}
}
// 휘핑 추가 커피
public class CoffeeWithWhip extends Coffee {
@Override
public int getCost() {
return 3000 + 700;
}
@Override
public String getDescription() {
return "아메리카노 + 휘핑";
}
}
// 샷 + 휘핑 추가 커피
public class CoffeeWithShotAndWhip extends Coffee {
@Override
public int getCost() {
return 3000 + 500 + 700;
}
@Override
public String getDescription() {
return "아메리카노 + 샷 + 휘핑";
}
}
// 샷 + 시럽 추가 커피...
// 휘핑 + 시럽 추가 커피...
// 샷 + 휘핑 + 시럽 추가 커피...
// 끝이 없다!!!
문제점:
- 조합이 늘어날수록 클래스가 폭발적으로 증가 (클래스 폭발)
- 동적으로 기능을 추가/제거할 수 없음
- 코드 중복이 심함
- 유지보수가 불가능
데코레이터 패턴은 "객체를 감싸서 기능을 추가하자"로 이 문제를 해결한다.
순수 Java로 구현하기
1단계: 컴포넌트 인터페이스 정의
// 커피 인터페이스
public interface Coffee {
int getCost();
String getDescription();
}
2단계: 구체적인 컴포넌트 (기본 커피)
// 기본 아메리카노
public class Americano implements Coffee {
@Override
public int getCost() {
return 3000;
}
@Override
public String getDescription() {
return "아메리카노";
}
}
// 기본 라떼
public class Latte implements Coffee {
@Override
public int getCost() {
return 4000;
}
@Override
public String getDescription() {
return "라떼";
}
}
3단계: 데코레이터 추상 클래스
// 추가 옵션의 기본 클래스
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee; // 감쌀 객체
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int getCost() {
return coffee.getCost();
}
@Override
public String getDescription() {
return coffee.getDescription();
}
}
4단계: 구체적인 데코레이터들
// 샷 추가
public class ShotDecorator extends CoffeeDecorator {
public ShotDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int getCost() {
return coffee.getCost() + 500; // 기존 가격 + 샷 가격
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 샷";
}
}
// 휘핑크림 추가
public class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int getCost() {
return coffee.getCost() + 700;
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 휘핑";
}
}
// 시럽 추가
public class SyrupDecorator extends CoffeeDecorator {
public SyrupDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int getCost() {
return coffee.getCost() + 300;
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 시럽";
}
}
// 우유 추가
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public int getCost() {
return coffee.getCost() + 500;
}
@Override
public String getDescription() {
return coffee.getDescription() + " + 우유";
}
}
5단계: 사용 예시
public class Main {
public static void main(String[] args) {
// 기본 아메리카노
Coffee coffee1 = new Americano();
System.out.println(coffee1.getDescription() + " = " + coffee1.getCost() + "원");
// 아메리카노 + 샷
Coffee coffee2 = new ShotDecorator(new Americano());
System.out.println(coffee2.getDescription() + " = " + coffee2.getCost() + "원");
// 아메리카노 + 샷 + 휘핑
Coffee coffee3 = new WhipDecorator(new ShotDecorator(new Americano()));
System.out.println(coffee3.getDescription() + " = " + coffee3.getCost() + "원");
// 라떼 + 샷 + 시럽 + 휘핑
Coffee coffee4 = new Latte();
coffee4 = new ShotDecorator(coffee4);
coffee4 = new SyrupDecorator(coffee4);
coffee4 = new WhipDecorator(coffee4);
System.out.println(coffee4.getDescription() + " = " + coffee4.getCost() + "원");
// 아메리카노 + 샷 2번 + 우유
Coffee coffee5 = new Americano();
coffee5 = new ShotDecorator(coffee5);
coffee5 = new ShotDecorator(coffee5); // 샷을 2번 추가!
coffee5 = new MilkDecorator(coffee5);
System.out.println(coffee5.getDescription() + " = " + coffee5.getCost() + "원");
}
}

장점:
- 새로운 조합을 만들 때 클래스를 추가할 필요 없음
- 동적으로 기능을 추가/제거 가능
- 같은 데코레이터를 여러 번 적용 가능 (샷 2번)
- 단일 책임 원칙 준수
실전 예제: 텍스트 에디터
더 실무적인 예시를 보자.
// 텍스트 인터페이스
public interface Text {
String getContent();
}
// 기본 텍스트
public class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
}
// 텍스트 데코레이터
public abstract class TextDecorator implements Text {
protected Text text;
public TextDecorator(Text text) {
this.text = text;
}
@Override
public String getContent() {
return text.getContent();
}
}
// 볼드 데코레이터
public class BoldDecorator extends TextDecorator {
public BoldDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<b>" + text.getContent() + "</b>";
}
}
// 이탤릭 데코레이터
public class ItalicDecorator extends TextDecorator {
public ItalicDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<i>" + text.getContent() + "</i>";
}
}
// 밑줄 데코레이터
public class UnderlineDecorator extends TextDecorator {
public UnderlineDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<u>" + text.getContent() + "</u>";
}
}
// 색상 데코레이터
public class ColorDecorator extends TextDecorator {
private String color;
public ColorDecorator(Text text, String color) {
super(text);
this.color = color;
}
@Override
public String getContent() {
return "<color=" + color + ">" + text.getContent() + "</color>";
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 기본 텍스트
Text text1 = new PlainText("Hello World");
System.out.println(text1.getContent());
// 볼드 텍스트
Text text2 = new BoldDecorator(new PlainText("Hello World"));
System.out.println(text2.getContent());
// 볼드 + 이탤릭
Text text3 = new ItalicDecorator(new BoldDecorator(new PlainText("Hello World")));
System.out.println(text3.getContent());
// 볼드 + 이탤릭 + 밑줄 + 색상
Text text4 = new PlainText("Hello World");
text4 = new BoldDecorator(text4);
text4 = new ItalicDecorator(text4);
text4 = new UnderlineDecorator(text4);
text4 = new ColorDecorator(text4, "red");
System.out.println(text4.getContent());
}
}
실전 예제: 알림 시스템
// 알림 인터페이스
public interface Notifier {
void send(String message);
}
// 기본 이메일 알림
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("📧 이메일 전송: " + message);
}
}
// 알림 데코레이터
public abstract class NotifierDecorator implements Notifier {
protected Notifier notifier;
public NotifierDecorator(Notifier notifier) {
this.notifier = notifier;
}
@Override
public void send(String message) {
notifier.send(message);
}
}
// SMS 알림 추가
public class SMSDecorator extends NotifierDecorator {
public SMSDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
super.send(message); // 기존 알림도 보냄
System.out.println("📱 SMS 전송: " + message);
}
}
// 카카오톡 알림 추가
public class KakaoDecorator extends NotifierDecorator {
public KakaoDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
super.send(message);
System.out.println("💬 카카오톡 전송: " + message);
}
}
// 슬랙 알림 추가
public class SlackDecorator extends NotifierDecorator {
public SlackDecorator(Notifier notifier) {
super(notifier);
}
@Override
public void send(String message) {
super.send(message);
System.out.println("💼 슬랙 전송: " + message);
}
}
// 사용
public class Main {
public static void main(String[] args) {
// 이메일만
Notifier notifier1 = new EmailNotifier();
notifier1.send("회원가입 완료");
System.out.println("\n--- VIP 고객 ---");
// 이메일 + SMS
Notifier notifier2 = new SMSDecorator(new EmailNotifier());
notifier2.send("특별 할인 안내");
System.out.println("\n--- 임직원 ---");
// 이메일 + 카카오톡 + 슬랙
Notifier notifier3 = new EmailNotifier();
notifier3 = new KakaoDecorator(notifier3);
notifier3 = new SlackDecorator(notifier3);
notifier3.send("긴급 공지사항");
}
}
데코레이터 패턴의 장점
- 개방-폐쇄 원칙: 새로운 기능 추가 시 기존 코드 수정 없음
- 단일 책임 원칙: 각 데코레이터는 하나의 기능만 담당
- 유연성: 런타임에 동적으로 기능 추가/제거
- 조합 가능: 여러 데코레이터를 자유롭게 조합
- 상속의 대안: 클래스 폭발 문제 해결
데코레이터 패턴의 단점
- 복잡도 증가: 작은 객체들이 많이 생성됨
- 순서 의존성: 데코레이터 적용 순서가 중요할 수 있음
- 디버깅 어려움: 여러 겹으로 감싸져 있어 추적이 어려움
- 특정 데코레이터 제거 어려움: 중간 데코레이터만 제거하기 힘듦
Java에서의 데코레이터 패턴
Java의 I/O 클래스가 대표적인 데코레이터 패턴 예시다.
// Java I/O의 데코레이터 패턴
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
// 기본 파일 입력
FileInputStream fis = new FileInputStream("file.txt");
// 버퍼링 기능 추가
BufferedInputStream bis = new BufferedInputStream(fis);
// 데이터 타입 변환 기능 추가
DataInputStream dis = new DataInputStream(bis);
// 모두 InputStream을 구현하지만 각자 다른 기능을 추가!
}
}
Spring에서의 데코레이터 패턴
// Spring의 TransactionTemplate 등이 데코레이터 패턴 활용
@Service
public class UserService {
private final UserRepository userRepository;
// 트랜잭션 데코레이터처럼 동작
@Transactional
public void createUser(User user) {
userRepository.save(user);
}
}
// 캐싱 데코레이터
@Cacheable("users")
public User getUser(Long id) {
return userRepository.findById(id);
}
// 로깅 데코레이터 (AOP)
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("메서드 실행 전");
Object result = joinPoint.proceed(); // 원본 메서드 실행
System.out.println("메서드 실행 후");
return result;
}
}
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 객체에 동적으로 기능을 추가/제거해야 할 때
- 기능의 조합이 많을 때
- 상속으로 인한 클래스 폭발을 피하고 싶을 때
- 런타임에 기능을 선택해야 할 때
사용하지 않아도 되는 경우:
- 기능 조합이 고정되어 있을 때
- 간단한 기능 추가만 필요할 때
- 데코레이터 순서가 중요하지 않을 때
핵심 원칙:
"기능을 동적으로 조합해야 한다면 데코레이터 패턴을 고려하라"
데코레이터 vs 어댑터 vs 프록시
| 패턴 | 목적 | 인터페이스 | 기능 |
| 데코레이터 | 기능 추가 | 동일 | 원본 + 추가 기능 |
| 어댑터 | 인터페이스 변환 | 다름 | 인터페이스만 변환 |
| 프록시 | 접근 제어 | 동일 | 제어 + 원본 기능 |
결론
데코레이터 패턴은 객체에 동적으로 기능을 추가하는 유연한 패턴이다.
기억해야 할 포인트:
- 객체를 감싸서 기능을 추가한다
- 기존 코드를 수정하지 않는다
- 기능을 자유롭게 조합할 수 있다
- 상속의 좋은 대안이다
- Java I/O, Spring AOP 등에서 활발히 사용된다
다음 글에서는 객체에 대한 접근을 제어하는 프록시 패턴을 알아볼 것이다.
다음 글 예고: 프록시 패턴 - 객체 접근을 제어하라
'디자인 패턴' 카테고리의 다른 글
| 전략 패턴(Strategy Pattern) (0) | 2026.01.14 |
|---|---|
| 프록시 패턴(Proxy Pattern) (0) | 2026.01.13 |
| 어댑터 패턴(Adapter Pattern) (0) | 2026.01.13 |
| 프로토타입 패턴(Prototype Pattern) (0) | 2026.01.13 |
| 빌더 패턴(Builder Pattern) (0) | 2026.01.13 |