JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 데코레이터 패턴 (Decorator)
1. 데코레이터 패턴이란? ☕
개념: 주어진 객체를 수정하지 않고, 동적으로 새로운 기능을 추가할 때 사용하는 패턴입니다. 객체를 새로운 기능을 가진 다른 객체로 감싸서(Wrapping) 기능을 확장합니다.
가장 대표적인 예는 커피 주문입니다.
- 기본 아메리카노를 주문합니다. (이것이 원본 객체입니다)
- 여기에 우유를 추가합니다. (우유 데코레이터가 아메리카노를 감쌉니다)
- 다시 그 위에 휘핑 크림을 올립니다. (휘핑 크림 데코레이터가 우유를 감싼 아메리카노를 감쌉니다)
각각의 '장식(데코레이터)'은 커피라는 본질을 바꾸지 않으면서 가격과 설명 같은 새로운 책임(기능)을 덧붙입니다.
2. 왜 사용할까요?
- 유연한 기능 확장: 상속을 사용하면 기능 확장이 정적으로 묶이지만, 데코레이터를 사용하면 런타임에 필요한 기능들을 원하는 대로, 원하는 순서로 조합하여 객체에 추가할 수 있습니다.
- 개방-폐쇄 원칙 (OCP) 충족: 기존 코드를 전혀 수정하지 않고도(Closed for modification) 데코레이터 클래스를 새로 만들어 기능을 확장(Open for extension)할 수 있습니다.
- 작은 단위의 기능 분리: 각 데코레이터는 단 하나의 기능만 책임지므로 코드가 간결하고 명확해집니다.
3. Java로 구현하는 방법
시나리오: 커피 주문 시스템을 만들어보겠습니다. 기본 커피에 우유, 시럽 등 다양한 토핑을 추가하는 기능입니다.
1단계: 핵심(Component) 인터페이스 정의
장식될 기본 객체와 장식(데코레이터)이 모두 구현해야 할 공통 인터페이스입니다.
// 모든 커피 객체가 공유하는 인터페이스
public interface Coffee {
double getCost(); // 가격을 반환
String getDescription(); // 설명을 반환
}
2단계: 구체적인 기본 객체(Concrete Component) 구현
장식의 기본이 될 원본 객체입니다.
// 기본 커피 (아메리카노)
public class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 4000; // 기본 가격 4000원
}
@Override
public String getDescription() {
return "아메리카노";
}
}
3단계: 추상 데코레이터(Decorator) 클래스 구현
모든 구체적인 데코레이터들이 상속받을 추상 클래스입니다. 내부적으로 자신이 감쌀 Coffee 객체를 멤버 변수로 가집니다.
// 모든 데코레이터의 부모 클래스
public abstract class CoffeeDecorator implements Coffee {
// 자신이 감쌀(Decoration) 커피 객체
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
// 기본적으로 자신이 감싸고 있는 객체에 요청을 위임한다.
public double getCost() {
return decoratedCoffee.getCost();
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
4단계: 구체적인 데코레이터(Concrete Decorator) 구현
실제 '장식' 역할을 하는 클래스들입니다. 부모인 CoffeeDecorator를 상속받아 기능을 확장합니다.
// 우유 추가 데코레이터
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
// 원래 가격에 우유 가격(500원)을 추가
return super.getCost() + 500;
}
@Override
public String getDescription() {
// 원래 설명에 '우유'를 추가
return super.getDescription() + ", 우유 추가";
}
}
// 휘핑 크림 추가 데코레이터
public class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double getCost() {
// 원래 가격에 휘핑 크림 가격(700원)을 추가
return super.getCost() + 700;
}
@Override
public String getDescription() {
// 원래 설명에 '휘핑 크림'을 추가
return super.getDescription() + ", 휘핑 크림 추가";
}
}
5단계: 클라이언트 코드에서 사용하기
클라이언트는 이제 원하는 대로 객체들을 조립(장식)하여 사용할 수 있습니다.
public class CoffeeShop {
public static void main(String[] args) {
// 1. 기본 아메리카노
Coffee simpleCoffee = new SimpleCoffee();
System.out.println("주문: " + simpleCoffee.getDescription() + " / 가격: " + simpleCoffee.getCost());
// 2. 아메리카노에 우유 추가 (라떼)
Coffee latte = new MilkDecorator(simpleCoffee);
System.out.println("주문: " + latte.getDescription() + " / 가격: " + latte.getCost());
// 3. 라떼에 휘핑 크림 추가
Coffee latteWithWhip = new WhipDecorator(latte);
System.out.println("주문: " + latteWithWhip.getDescription() + " / 가격: " + latteWithWhip.getCost());
// 4. 아메리카노에 휘핑 크림만 추가한 후 우유 추가 (순서 변경 가능)
Coffee coffeeWithWhipAndMilk = new MilkDecorator(new WhipDecorator(new SimpleCoffee()));
System.out.println("주문: " + coffeeWithWhipAndMilk.getDescription() + " / 가격: " + coffeeWithWhipAndMilk.getCost());
}
}
4. 데코레이터 패턴과 실제 사례
- Java I/O 클래스: 자바의 입출력 클래스는 데코레이터 패턴의 교과서적인 예입니다.
- new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")))
- FileInputStream(기본 객체)을 InputStreamReader(문자 스트림으로 변환하는 데코레이터)로 감싸고, 다시 이것을 BufferedReader(버퍼 기능 추가 데코레이터)로 감싸서 기능을 점층적으로 확장합니다.
- 스프링 프레임워크: 스프링에서는 AOP(관점 지향 프로그래밍)를 구현하거나 트랜잭션, 보안 등을 적용할 때 내부적으로 프록시(Proxy) 객체를 사용하는데, 이 프록시가 데코레이터 패턴과 매우 유사한 방식으로 동작하여 원래의 빈(Bean) 객체에 부가 기능을 추가합니다.
5. 문제 제시 및 답변 💡
문제: 간단한 텍스트를 표시하는 TextView 컴포넌트가 있습니다. 이 TextView에 스크롤바(Scrollbar)를 추가하거나, 테두리(Border)를 추가하는 기능을 넣고 싶습니다. 상속을 사용하지 않고 데코레이터 패턴을 사용하여 이 기능을 구현하려면 어떻게 설계해야 할까요?
답변:
커피 예제와 동일한 구조로 설계할 수 있습니다.
- Component 인터페이스: draw() 메서드를 가진 공통 인터페이스를 만듭니다.
- TextView 클래스: Component 인터페이스를 구현하는 기본 텍스트 컴포넌트를 만듭니다. draw() 메서드는 단순히 텍스트를 출력합니다.
- Decorator 추상 클래스: Component를 구현하고, 내부에 Component 객체를 멤버로 가집니다.
- ScrollbarDecorator 클래스: Decorator를 상속받습니다. draw() 메서드에서 먼저 스크롤바를 그린 후, 감싸고 있는 Component 객체의 draw()를 호출합니다.
- BorderDecorator 클래스: Decorator를 상속받습니다. draw() 메서드에서 먼저 테두리를 그린 후, 마찬가지로 내부 Component의 draw()를 호출합니다.
사용 예시: new BorderDecorator(new ScrollbarDecorator(new TextView())) 와 같이 호출하면, 텍스트 뷰에 스크롤바가 추가되고 그 전체에 다시 테두리가 그려지는 객체를 동적으로 생성할 수 있습니다.
'프로그래밍 > 알고리즘&자료구조&패턴' 카테고리의 다른 글
| 디자인패턴 - 프록시 패턴 (1) | 2025.08.18 |
|---|---|
| 디자인패턴 - 퍼사드 패턴 (2) | 2025.08.18 |
| 디자인 패턴 - 어댑터 패턴 (1) | 2025.08.18 |
| 디자인패턴 - 빌더패턴 (1) | 2025.08.18 |
| 디자인패턴 - 팩토리패턴 (1) | 2025.08.18 |