데코레이터(Decorator) 패턴은 객체의 기능을 동적으로 확장할 수 있도록 하는 디자인 패턴입니다.
여러분은 스타벅스 알바생입니다. 음료를 만드는 데 꽤나 힘이 드니 3가지 음료만 만듭니다. 에스프레소 베이스 메뉴, 말차 베이스 메뉴가 있고, 적절한 토핑과 함께하면 아래와 같이 맛있는 메뉴가 탄생합니다!
베이스 2종(에스프레소, 말차)과 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 적절히 조합해 커피를 판매해보려고 합니다. 이 때, 3가지 종류의 토핑은 기본 베이스를 확장해주는 데코레이터입니다.
먼저 커피 인터페이스를 만들어줍니다.
public interface Coffee {
int getPrice();
}
이제 커피 인터페이스를 구현해 베이스 2종을 만들어주겠습니다. 에스프레소 베이스는 4000원, 말차 베이스는 5000원입니다.
public class Espresso implements Coffee{
@Override
public int getPrice() {
return 4000;
}
}
public class Matcha implements Coffee{
@Override
public int getPrice() {
return 5000;
}
}
베이스 두 개 완성!
이번에는 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 만들어보겠습니다.
토핑은 토핑만으로 커피가 될 수 없고, 반드시 베이스와 결합되어야 합니다. 그렇기 때문에 내부에 베이스 커피가 포함되어야 하고, 필드로 Coffee
를 갖고 있어야 합니다. 또한 기본 생성자가 아닌 Coffee
를 파라미터로 받는 생성자를 통해서만 Milk
객체를 만들 수 있습니다.
그리고, 베이스 음료에 우유를 넣게 되면, 가격은 기존 음료에서 500원이 추가됩니다.
public class Milk implements Coffee {
private Coffee coffee;
public Milk(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int getPrice() {
return coffee.getPrice() + 500;
}
}
동일한 방식으로, 글레이즈드 폼과 카라멜 시럽도 만들어보겠습니다.
public class GlazedFoam implements Coffee {
private Coffee coffee;
public GlazedFoam(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int getPrice() {
return coffee.getPrice() + 300;
}
}
public class CaramelSyrup implements Coffee{
private Coffee coffee;
public CaramelSyrup(Coffee coffee) {
this.coffee = coffee;
}
@Override
public int getPrice() {
return coffee.getPrice() + 400;
}
}
자! 이제 모든 준비가 끝났습니다. 베이스 2종(에스프레소, 말차)과 3가지 토핑(우유, 글레이즈드 폼, 카라멜 시럽)을 적절히 조합해 커피를 만들어보겠습니다.
커피 공장에서 라떼를 만들어보겠습니다. 라떼는 에스프레소 베이스에, 우유를 넣으면 됩니다! 에스프레소 베이스가 4000원, 우유가 500원이기 때문에 라떼의 가격은 4500원입니다.
public class CoffeeFactory {
public static Coffee latte() {
return new Milk(new Espresso());
}
}
에스프레소 베이스를 우유 데코레이터로 래핑시켜주었더니 라떼가 만들어졌습니다!
테스트 코드를 통해 4500원이 맞는지 확인해봅니다.
class CoffeeFactoryTest {
@Test
void makeLatte() {
Coffee latte = CoffeeFactory.latte();
assertThat(latte.getPrice()).isEqualTo(4500);
}
}
마찬가지로 카라멜 라떼, 말차 글레이즈드 라떼도 만들어보겠습니다.
카라멜 라떼는 에스프레소 베이스(4000원
)에 우유(500원
), 카라멜 시럽(400원
)을 추가하면 되며, 가격은 4900원입니다.
말차 글레이즈드 라떼는 말차 베이스에 우유(500원
), 글레이즈드 폼(300원
), 카라멜 시럽(400원
)을 추가하면 되며, 가격은 6200원입니다.
public class CoffeeFactory {
// 라떼 생략
public static Coffee caramelLatte() {
return new CaramelSyrup(new Milk(new Espresso()));
}
public static Coffee matchaGlazedLatte() {
return new GlazedFoam(new CaramelSyrup(new Milk(new Matcha())));
}
}
코드를 보면, 말차 글레이즈드라떼를 만들기 위해 여러 데코레이터가 재귀적으로 말차 베이스를 래핑하고 있습니다.
이처럼 데코레이터 패턴은 객체의 기능을 확장하거나 변경해야 할 때, 객체를 재귀적으로 결합하여 서브 클래스가 폭발적으로 많아지는 문제를 해결할 수 있는 구조적(Structural) 디자인 패턴입니다.
지금은 세 가지 메뉴 조합만 있지만, 바닐라 시럽, 초코 드리즐, 자바칩, 모카 소스, 휘핑크림, . . . 등 데코레이터가 늘어난다면 만들 수 있는 메뉴도 기하급수적으로 늘어나게 됩니다.
자바칩 초코 프라푸치노는 에스프레소 베이스에 모카 소스, 휘핑크림, 초코 드리즐을 추가하면 만들 수 있습니다. 이 외에 바닐라 라떼, 초코 바닐라 라떼, 자바칩 프라푸치노,초코 라떼 등 수많은 커피 메뉴를 탄생시킬 수 있습니다!
커피 공장에서 카라멜 라떼를 만들어 4900원인지 확인하고, 말차 글레이즈드라떼를 만들어 6200원이 맞는지 테스트 코드를 통해 확인해보겠습니다.
class CoffeeFactoryTest {
// 라떼 테스트 코드 생략
@Test
void makeCaramelLatte() {
Coffee caramelLatte = CoffeeFactory.caramelLatte();
assertThat(caramelLatte.getPrice()).isEqualTo(4900);
}
@Test
void makeMatchaGlazedLatte() {
Coffee matchaGlazedLatte = CoffeeFactory.matchaGlazedLatte();
assertThat(matchaGlazedLatte.getPrice()).isEqualTo(6200);
}
}
모든 테스트가 성공했습니다!
그럼 지금까지 구현한 내용을 클래스 다이어그램으로 그려보겠습니다.
베이스 2종과 토핑 3종 모두 커피 인터페이스를 구현하고 있으며, 토핑 3종(Milk
, GlazedFoam
, CaramelSyrup
)은 베이스 2종(Espresso
, Matcha
)에 결합될 수 있는 데코레이터로서 내부 필드로 커피 타입을 갖고 있습니다.
그런데 저희가 흔히 보는 데코레이터 패턴의 클래스 다이어그램은 저렇게 생기지 않았던 것 같은데요. 이제 데코레이터 추상 클래스를 추가해서 우유, 글레이즈드폼, 카라멜 시럽이 상속받도록 변경해 토핑 3종이 데코레이터라는 것을 조금 더 명확하게 드러내보겠습니다.
추상 클래스는 추상 메서드와 변수를 가질 수 있습니다. 위에 구현한 내용을 아래 그림처럼 바꿔보겠습니다.
CoffeeDecorator
는 추상 클래스로, Coffee
인터페이스를 구현합니다. 이때 Coffee
인터페이스의 getPrice()
메서드의 구현은 Milk
, GlazedFoam
, CaramelSyrup
클래스에서 하게 됩니다.
커피 토핑은 반드시 베이스가 있어야 하기 때문에, 필드에 Coffee
를 선언하고, Coffee
타입을 받는 생성자를 만들어줍니다.
public abstract class CoffeeDecorator implements Coffee {
Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
❓
Coffee
인터페이스를 구현했는데 왜getPrice()
메서드를 오버라이드하라는 에러가 안뜨지
CoffeeDecorator
는 추상 클래스이기 때문에 이 자체로 객체를 만들 수 없습니다. 결국 다른 구체(concrete) 클래스가 CoffeeDecorator
를 구현해줘야 합니다. 추상 클래스이기 때문에, 추상 메서드를 가질 수 있습니다. CoffeeDecorator
가 인터페이스에 있는 메서드를 구현해도 되지만, 그 아래의 구체 클래스에서 구현해도 됩니다.
이제 CoffeeDecorator
추상 클래스를 상속 받는 Milk
, GlazedFoam
, CaramelSyrup
을 만들어보겠습니다.
CoffeeDecorator
추상클래스를 상속받는 Milk
클래스를 만들고, Coffee
인터페이스에 있던 getPrice()
메서드를 구현해줍니다.
이때 Milk
는 상위 클래스인CoffeeDecorator
로부터 coffee
인스턴스 필드를 상속 받아 아래와 같이 사용할 수 있습니다.
public class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public int getPrice() {
return coffee.getPrice() + 500;
}
}
앗. . 그런데 에러가 났습니다.
패키지 구조를 살펴보겠습니다.
CoffeeDecorator
와 Milk
는 다른 패키지에 있습니다. CoffeeDecorator
의 coffee
는 default 접근 제어자로 설정 되어 있기 때문에 다른 패키지에서는 접근할 수 없습니다. 따라서 protected 접근제어자로 바꿔줍니다.
protected 접근 제어자를 사용하면, 패키지 위치가 다른 자식 클래스에서 해당 필드에 접근이 가능합니다.
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
// 생성자 생략
}
에러가 해결됐습니다!
이제 글레이즈드폼과 카라멜 시럽도 마저 만들어주겠습니다.
public class GlazedFoam extends CoffeeDecorator {
public GlazedFoam(Coffee coffee) {
super(coffee);
}
@Override
public int getPrice() {
return coffee.getPrice() + 300;
}
}
public class CaramelSyrup extends CoffeeDecorator {
public CaramelSyrup(Coffee coffee) {
super(coffee);
}
@Override
public int getPrice() {
return coffee.getPrice() + 400;
}
}
커피 공장, 에스프레소, 말차, 테스트 코드는 그대로입니다.
테스트 코드를 돌려 라떼, 카라멜 라떼, 말차 글레이즈드 라떼가 잘 만들어지는지 확인해보겠습니다.
커피를 열심히 만들면서 데코레이터 패턴에 대해 알아봤습니다. 말차 글레이즈드 라떼는 참 맛있습니다.
다음 번에는 데코레이터 패턴과 구조는 동일하지만 용도가 다른 프록시 패턴을 알아보겠습니다람쥐
와~ 말차글레이즈드라떼가 땡기는 글이네요 좋은 글 감사합니다!!