정의
데코레이터(Decorator)는 사전적으로 무언가를 꾸며준다는 의미를 가지고 있다.
이에 착안해 데코레이터 패턴의 사전적 정의를 살펴보면 다음과 같다.
객체에 추가 요소를 동적으로 더할 수 있도록 하는 패턴으로, 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.
데코레이터 패턴은 Java I/O 에서 대표적으로 적용되고 있는 디자인 패턴이다. 사전적 정의가 실제 구현에서 어떻게 사용되는 것인지 와 닿기 힘들다. 이에 관련된 문제 상황과 해결 방법을 아래 문단에서 자세하게 알아보자.
문제 상황
우리는 샐러드 가게에서 사용할 전산 시스템을 만드는 상황을 가정해 보자.
만약 아래와 같이 샐러드 주문 표가 있다고 할 때, 각 샐러드 조합에 대한 클래스를 만드려면 어떻게 해야 할까?
만약 조합된 샐러드의 정보를 출력하는 메소드를 getDescription(), 가격을 출력하는 메소드를 cost() 라고 한다면
기본적으로 아래와 같은 클래스 조합을 만들 수 있을 것이다.
한 눈에 봐도 문제점을 찾을 수 있다고 생각된다. 이러한 방식에서는 어떤 문제점이 있을까?
대표적인 문제점을 나열하면 아래와 같이 정리할 수 있다.
- 만약 치킨 샐러드에 계란을 2개 추가한다면, ChickenSaladWithBoiledEggWithBoiledEgg 와 같은 추가적인 클래스가 필요할 것이다.
- 만약 샐러드의 종류나 토핑의 종류가 하나만 추가되어도 매우 많은 클래스가 새로 만들어져야 한다.
- 즉, 가능한 조합이 무수히 많기 때문에 클래스를 매우 많이 만들어야 하는데, 이는 현실적으로 불가능에 가깝다.
많은 문제점이 있겠지만, 위의 문제점 만으로도 이러한 방식은 실제로 적용할 수 없는 방법임을 알 수 있을 것이다.
그렇다면 어떤 접근 방법으로 이러한 문제를 해결해야 할까? 이 때 사용할 수 있는 것이 데코레이터 패턴이다.
데코레이터 패턴 적용하기
데코레이터 패턴은 같은 슈퍼 클래스를 사용하고 있는 객체를 또 다른 객체가 감싸는(wrapped) 방식으로 문제를 해결한다.
예를 들어서, 만약 고객이 소고기 샐러드에 계란과 아보카도를 추가하는 상황을 생각해 보자.
이는 데코레이터 패턴의 입장에서 생각한다면
- Beef Salad 객체를 Avocade 객체가 감싸고, 이 객체를 다시 Boiled Egg 객체가 감싸도록 구성한다.
로 해석될 수 있다는 것이다. 이를 그림으로 표현하면 다음과 같다.
그림을 보면 알 수 있듯이, 객체를 객체가 감싸는 구조를 통해서 만약 cost() 라는 가격을 확인하는 메소드를 호출한다면
마치 재귀함수처럼 자신이 감싸고 있는 객체의 cost() 메소드를 다시 호출해서 최종 결과를 도출해낸다는 것을 알 수 있다.
이러한 구조를 적용하기 위해서는 상단에 서술한 바와 같이 같은 슈퍼 클래스 자료형을 사용하고 있어야 하는데,
문제 상황에서 이러한 구조를 적용한다면 아래와 같은 클래스 다이어그램을 얻을 수 있을 것이다.
이렇게 얻은 클래스 다이어그램을 정리해보면,
- Salad 라는 슈퍼 클래스를 가진다.
- Salad 클래스에서 파생되어 다른 객체(토핑)에게 감싸질 ChickenSalad, BeefSalad 클래스를 생성한다.
- Salad는 다른 객체(토핑)에게 감싸지므로, 토핑을 CondimentSalad 클래스로 만들고, 이는 Salad 슈퍼클래스 자료형을 멤버 변수로 가진다.
- CondimentSalad 클래스의 서브 클래스들로 토핑 클래스를 생성한다.
이러한 방식으로 샐러드를 감싸는 토핑 객체가 계속해서 Salad 인스턴스를 감싸는 형태로 객체에 추가 요소를 동적으로 확장할 수 있다.
구현
public abstract class Salad {
public String description = "Untitled";
public String getDescription() {
return description;
}
public abstract double cost();
}
public class ChickenSalad extends Salad {
public ChickenSalad() {
this.description = "ChickenSalad";
}
public double cost() {
return 4.9;
}
}
public class BeefSalad extends Salad {
public BeefSalad() {
this.description = "BeefSalad";
}
public double cost() {
return 6.9;
}
}
public abstract class CondimentSalad extends Salad {
Salad salad;
public abstract String getDescription();
}
public class BoiledEgg extends CondimentSalad {
BoiledEgg(Salad salad) {
this.salad = salad;
}
public String getDescription() {
return salad.getDescription() + ", BoiledEgg";
}
public double cost() {
return salad.cost() + 0.9;
}
}
public class CheddarCheese extends CondimentSalad {
CheddarCheese(Salad salad) {
this.salad = salad;
}
public String getDescription() {
return salad.getDescription() + ", Cheddar Cheese";
}
public double cost() {
return salad.cost() + 1.9;
}
}
public class Avocado extends CondimentSalad {
public Avocado(Salad salad) {
this.salad = salad;
}
public String getDescription() {
return salad.getDescription() + ", Avocado";
}
public double cost() {
return salad.cost() + 2.9;
}
}
public class SaladCafeTest {
public static void printSalad(Salad salad) {
System.out.println("[Salad Information]");
System.out.println("- Name: " + salad.getDescription());
System.out.println("- Cost: $" + Math.floor(salad.cost() * 10) / 10 + "\n");
}
public static void main(String[] args) {
Salad chickenSalad = new ChickenSalad();
chickenSalad = new BoiledEgg(chickenSalad);
chickenSalad = new Avocado(chickenSalad);
printSalad(chickenSalad);
Salad beefSalad = new BeefSalad();
printSalad(beefSalad);
beefSalad = new BoiledEgg(beefSalad);
beefSalad = new BoiledEgg(beefSalad);
beefSalad = new CheddarCheese(beefSalad);
printSalad(beefSalad);
}
}
참고
헤드 퍼스트 디자인 패턴, 한빛 미디어
'Programming > Design Pattern' 카테고리의 다른 글
[디자인패턴] 옵저버 패턴(Observer Pattern)이란 무엇인가? (0) | 2023.03.08 |
---|---|
[디자인패턴] 전략 패턴(Strategy Pattern)이란 무엇인가? (0) | 2023.03.07 |