디자인 패턴 & OOP
[디자인 패턴] 데코레이터 패턴(Decorator Pattern)
cloud-grace
2024. 5. 19. 15:48
데코레이터 패턴(Decorator Pattern)이란?
데코레이터 패턴은 디자인 패턴(Design Pattern) 중 구조 패턴(Structural Pattern)이다.
구조 패턴 : 클래스나 객체를 조합해서 더 큰 구조를 만드는 패턴
예를 들어, 서로 다른 인터페이스를 지닌 객체 2개를 묶어서 단일 인터페이스를 제공하거나 객체들을 서로 묶어서 새로운 기능을 제공하는 패턴이다.
GoF 디자인 패턴에 의하면 데코레이터 패턴은 주어진 상황 및 용도에 따라 어떤 객체에 다른 객체를 덧붙이는 방식이다.
사용 목적은 클래스의 요소들을 계속 수정하면서 사용하는 구조가 필요할 때, 여러 요소들을 조합해서 사용하는 구조일 때 사용한다. 예를 들어, 캐싱, 로깅, 검증과 같은 기능에 사용된다.
데코레이터 패턴 : 주어진 상황과 용도에 따라 어떤 객체에 책임(기능)을 덧붙이는 패턴으로, 기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다. 말 그대로 장식이라고 생각하면 된다. 기본 기능을 가지고 있는 클래스를 만들고 여기다가 추가할 수 있는 기능들을 추가하기 편하도록 설계하는 방식이다.
따라서 데코레이터 패턴은 기본 틀에 새롭게 추가될 수 있는 정보들을 따로 분리하면서, 확장하기 용이하도록 융통성을 제공한다. 기존 객체를 '행위'를 가진 특별한 래퍼 객체(데코레이터)에 넣어 객체가 그 '행위'를 할 수 있게 만들어준다.
데코레이터 패턴 구조
- Component : 부모 Class로 구성 요소들을 의미하는 추상 클래스이자, 구체적인 객체와 데코레이터가 구현해야 하는 인터페이스이다. operation을 하위 클래스가 재정의하도록 한다. 실질적인 인스턴스를 컨트롤하는 역할을 한다.
- ConcreteComponent : 기본 기능을 구현하는 구체적인 클래스이다. Component의 실질적인 인스턴스 부분으로 책임의 주체 역할을 한다.
- Decorator : 인터페이스를 구현하며, Component 객체를 포함하는 추상 클래스이다. 부수적인 기능, 장식이 되는 기능을 추가한다. Component와 ConcreteDecorator를 동일하게 해주는 역할을 한다.
- ConcreteDecorator : 데코레이터의 구체적인 구현 클래스이다. Decorator를 상속 받아 구현한다. 상속이 아닌 aggregation을 이용한다는 점에서 더욱 융통적이다. 즉, 이 클래스는 Component 객체에 새로운 기능을 추가한다. 실질적인 장식 인스턴스와 정의이며 추가된 책임의 주체이다.
데코레이터 패턴 예제 코드
예제 : 커피 제조
- Component : 커피를 제조할 때 비용과 재료 설명을 필요로 하며 이 함수를 Component 인터페이스에서 구현한다.
interface Coffee {
double cost(); // 비용
String description(); // 재료 설명
}
- ConcreteComponent : Component를 상속 받아 기본 커피의 비용은 5.0, 재료 설명은 Simple Coffee로 정의한다.
class SimpleCoffee implements Coffee {
@Override
public double cost() {
return 5.0;
}
@Override
public String description() {
return "Simple Coffee";
}
}
- Decorator : 커피 비용과 재료 설명의 근간이 되는 추상 클래스이다. 재료는 이 Decorator를 상속 받아 재료를 추가한다.
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
@Override
public double cost() {
return decoratedCoffee.cost();
}
@Override
public String description() {
return decoratedCoffee.description();
}
}
- ConcreteDecorator : MilkDecorator는 우유를 추가하는 것으로 cost()와 description()을 정의해준다.
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return decoratedCoffee.cost() + 1.5;
}
@Override
public String description() {
return decoratedCoffee.description() + ", Milk";
}
}
- ConcreteDecorator : SugarDecorator는 설탕을 추가하는 것으로 cost()와 description()을 정의해준다.
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public double cost() {
return decoratedCoffee.cost() + 0.5;
}
@Override
public String description() {
return decoratedCoffee.description() + ", Sugar";
}
}
- Main : 클라이언트 코드는 SimpleCoffee 객체를 생성하고 필요에 따라 데코레이터를 적용하여 커피의 기능을 확장한다.
public class Main {
public static void main(String[] args) {
Coffee simpleCoffee = new SimpleCoffee();
System.out.println(simpleCoffee.description() + " : $" + simpleCoffee.cost());
// Simple Coffee : $5.0
Coffee milkCoffee = new MilkDecorator(simpleCoffee);
System.out.println(milkCoffee.description() + " : $" + milkCoffee.cost());
// Simple Coffee, Milk : $6.5
Coffee milkSugarCoffee = new SugarDecorator(milkCoffee);
System.out.println(milkSugarCoffee.description() + " : $" + milkSugarCoffee.cost());
// Simple Coffee, Milk, Sugar : $7.0
}
}
- 데코레이터 패턴을 사용하면 자기가 감싸고 있는 구성 요소의 메소드를 호출한 결과에 새로운 기능을 더하면서 행동을 확장할 수 있다.
데코레이터 패턴의 장점
- 데코레이터(래퍼 클래스)를 이용해 기능을 조합할 수 있다.
- 상속은 조합이 불가능해서 유연하지 못했지만, 조합 순서에도 상관없이 조합이 가능하다.
- 또한, 컴파일 타임에 기능 내용이 확정되는 것이 아닌, 런타임에 기능 내용을 변경할 수 있다.
- 상속의 경우에는 이미 컴파일 타임에 관계가 확정되어 있다.
데코레이터 패턴의 단점
- 데코레이터를 조합하는 코드가 조금 복잡해질 수 있다.
- 작은 객체들이 많이 늘어나지만 상속보다는 많이 늘어나지는 않는다.