이 글을 읽고 계시는 여러분을 유명한 햄버거 체인의 백엔드 직원으로 상상해 봅시다.
문제 상황
맥도날드, 롯데리아, 버거킹만 해도 햄버거의 다양성이 상상을 초월합니다. 그리고 햄버거에는 패티 추가, 양상추 추가, 양파 빼기 등 수많은 옵션들이 있습니다.
어느 날 여러분에게 온라인 햄버거 주문 시스템을 개발하라는 지시가 내려졌습니다.
이런 상황에서 여러분은 어떻게 이러한 다양한 햄버거를 구현할 것인가요?
아마 데코레이터 패턴을 알지 못하시는 분들은 "공통 인터페이스를 만든 뒤, 구현하면 되죠!!"라고 말씀을 하실 것이라고 생각됩니다. 밑의 그림은 책에 나온 예시입니다. 비록 햄버거는 아니었지만 커피를 예시로 다이어그램을 그린 그림입니다. 정말 수많은 클래스들이 하나의 인터페이스를 구현하는 모습을 볼 수 있습니다.
양파가 있는 싸이버거, 양파가 없는 싸이버거, 패티 추가한 싸이버거......
이렇게 구현하신다면 클래스가 폭발하는 모습을 볼 수 있습니다....
데코레이터 패턴이란?
데코레이터 패턴은 객체에 추가 요소를 동적으로 더할 수 있게 하는 패턴입니다.
조금 더 쉽게 말하자면 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 방식입니다.
데코레이터 패턴 예시
Beverage
커피 클래스가 구현하는 추상 클래스입니다.
public abstract class Beverage {
String description = "제목 없음";
public String getDescription() {
return description;
}
public abstract double cost();
}
CondimentDecorator
첨가물 클래스가 구현하는 추상 클래스입니다.
public abstract class CondimentDecorator extends Beverage {
Beverage beverage;
public abstract String getDescription();
}
Q : CondimentDecorator에서 Bevarage를 확장하고 있는데 이는 상속이 아닌가요? 상속은 컴파일 시 정적으로 결정되는 것으로 알고 있는데,,
A : 상속이 맞습니다. 하지만 행동을 상속받으려고 확장하는 것이 아닌 형식을 맞추려고 확장하는 것입니다. 또한 인스턴스 변수로 다른 객체를 저장하고 있기 때문에 동적으로 행동할 수 있습니다.
Bevarage를 확장한 에스프레소 클래스입니다.
public class Espresso extends Beverage {
public Espresso() {
description = "Espresso";
}
public double cost() {
return 1.99;
}
}
이때는 음료의 설명, 가격에 대한 구현만 하면 됩니다.
첨가물 데코레이터를 확장한 모카 클래스입니다.
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage)
this.beverage = beverage;
}
public String getDescription()
return beverage.getDescription() + ", Mocha";
}
public double cost() {
return .20 + beverage.cost();
}
}
Mocha의 인스턴스에는 Bevarage의 레퍼런스가 들어가 있습니다.
그러므로 모카를 추가하고자 하는 음료가 생성자의 인자로 필요합니다.
설명에서는 생성자에서 인자로 받은 클래스의 설명에 Mocha를 추가하고, 가격에서도 인자로 받은 클래스의 가격에. 20달러를 추가하는 모습을 볼 수 있습니다.
이렇게 Decorator 패턴은 객체(Beverage)에 추가 요소(Mocha)를 쉽게 추가할 수 있게 해 줍니다.
실행 코드
public class StarbuzzCoffee {
public static void main(String args[]) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription()
+ " $" + beverage.cost());
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription()
+ " $" + beverage2.cost());
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription()
+ " $" + beverage3.cost());
}
}
Bevarage2를 보면 리스코프 치환원칙에 의하여 DarkRoast 객체로 생성한 뒤, Mocha의 생성자에 인자로 두 번 보내지고, Whip의 생성자에 인자로 두번 보내진 모습을 볼 수 있습니다.
데코레이터 패턴을 이용하면 옵션 별 새로운 클래스를 생성하지 않아도 옵션의 생성자에 객체를 보냄으로써 쉽게 추가적인 요소를 추가할 수 있습니다.
출력 값
Espresso $1.99
Dark Roast, Mocha, Mocha, Whip $1.49
House Blend, Soy, Mocha, Whip $1.34