김코딩

데코레이터 패턴(Decorator Pattern) 본문

디자인 패턴

데코레이터 패턴(Decorator Pattern)

김코딩딩 2026. 1. 13. 21:52

디자인 패턴 시리즈의 일곱 번째 주제는 데코레이터 패턴(Decorator Pattern)이다.

 

어댑터 패턴"인터페이스를 변환"하는 거였다면, 데코레이터 패턴"기능을 추가"하는 것이다.

 

예를 들어, 커피숍에서 아메리카노를 주문한다고 생각해보자. 기본 아메리카노에 샷 추가, 휘핑크림 추가, 시럽 추가... 이렇게 옵션을 덧붙이면 가격도 올라가고 기능도 추가된다. 데코레이터 패턴은 이처럼 객체에 기능을 장식(Decorate)하듯이 추가하는 패턴이다.

 

이번 글에서는 데코레이터 패턴무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.


데코레이터 패턴의 정의

데코레이터 패턴(Decorator Pattern)객체에 추가적인 기능을 동적으로 첨가하는 패턴이다.

기존 코드를 수정하지 않고, 객체를 감싸서 새로운 기능을 추가할 수 있다.

 

핵심 특징:

  1. 객체에 동적으로 기능을 추가한다
  2. 기존 코드를 수정하지 않는다
  3. 기능을 조합할 수 있다
  4. 래퍼(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 "아메리카노 + 샷 + 휘핑";
    }
}

// 샷 + 시럽 추가 커피...
// 휘핑 + 시럽 추가 커피...
// 샷 + 휘핑 + 시럽 추가 커피...
// 끝이 없다!!!

 

문제점:

  1. 조합이 늘어날수록 클래스가 폭발적으로 증가 (클래스 폭발)
  2. 동적으로 기능을 추가/제거할 수 없음
  3. 코드 중복이 심함
  4. 유지보수가 불가능

데코레이터 패턴"객체를 감싸서 기능을 추가하자"로 이 문제를 해결한다.


순수 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("긴급 공지사항");
    }
}

데코레이터 패턴의 장점

  1. 개방-폐쇄 원칙: 새로운 기능 추가 시 기존 코드 수정 없음
  2. 단일 책임 원칙: 각 데코레이터는 하나의 기능만 담당
  3. 유연성: 런타임에 동적으로 기능 추가/제거
  4. 조합 가능: 여러 데코레이터를 자유롭게 조합
  5. 상속의 대안: 클래스 폭발 문제 해결

데코레이터 패턴의 단점

  1. 복잡도 증가: 작은 객체들이 많이 생성됨
  2. 순서 의존성: 데코레이터 적용 순서가 중요할 수 있음
  3. 디버깅 어려움: 여러 겹으로 감싸져 있어 추적이 어려움
  4. 특정 데코레이터 제거 어려움: 중간 데코레이터만 제거하기 힘듦

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