[컴퓨터공학]/[Design Pattern]

[Design Pattern] 데코레이터 패턴 - Decorator Pattern

딥러닝 도전기 2022. 10. 8. 19:58

[Design Pattern] 데코레이터 패턴 - Decorator Pattern 

 

이번 포스팅에서는 구조 패턴중 한 개인 데코레이터 패턴에 대해 알아보도록 하겠습니다.

다음의 교재를 참고했음을 미리 밝힙니다.

헤드퍼스트 디자인 패턴 - O'REILLY

 


데코레이터 패턴은 다음과 같은 특징을 갖습니다.

  • 기능을 계속해서 추가할 수 있는 패턴
  • 기능을 실행중에 동적으로 변경, 확장할 수 있는 패턴
  • 한 객체를 여러개의 데코레이터로 장식할 수 있다.
  • 객체를 언제든지 감쌀 수 있기 때문에 실행중에 필요한 데코레이터를 마음대로 적용할 수 있다.

데코레이터 패턴의 Class diagram은 다음과 같습니다.

 

 

 

위의 Class diagram을 기반으로 Coffee house의 예시로 설명하겠습니다.

 

우선, 초창기의 Coffee house의 주문 시스템 클래스가 다음과 같이 구성되어 있다고 생각해보겠습니다.

 

Beverage 클래스를 상속받는 HouseBlend, DarkRoast, Decaf, Espresso

 

여기에 각종 첨가물(우유, 두유, 모카, 휘핑크림 등)을 추가하여 만든 클래스가 다음과 같다고 하면 어떤 문제가 있을까요?

 

 

모든 커피 종류Class들이 Beverage라는 super class를 상속받는다고 생각해보면, 위의 경우는 다음과 같은 문제가 있습니다.

1. 첨가물의 가격이 변경되었을 때 많은 클래스를 수정해야한다.

2. 새로운 첨가물을 추가하기 번거롭다.

$\to$ OCP를 위배한다.

 

위와 같이 각각의 첨가물마다 클래스를 설정하면 클래스의 수가 너무 많아질 뿐만 아니라, 객체지향 프로그래밍 원칙인 OCP(Open-Closed Principle)을 위배하게 됩니다. 새로운 첨가물(ex. 헤이즐넛)이 추가된다고 하면, 또다시 많은 양의 클래스가 늘어나게 됩니다.

 

 

그렇다면 상속의 문제점에 대한 해결 방안으로 인스턴스 변수를 사용한다고 생각해보겠습니다.

인스턴스 변수로 각종 첨가물을 설정하고, getter와 setter를 설정한다고 하면 다음과 같은 class diagram을 얻습니다.

 

위와 같은 코드로 변경하니 클래스의 수는 확연히 줄었습니다.

하지만 어떤 문제가 있을까요?

 

1. 여전히 OCP를 위배한다는 문제점이 있습니다. 만약 첨가물의 가격이 바뀐다면, 기존 클래스를 수정해야 하기 때문입니다.

 

2. 또한, 첨가물의 종류가 더 많아지면 새로운 메소드 (ex, 헤이즐넛이 추가된다면 인스턴스 변수에 +hazelnut을, 메소드에 +hasHazelnut(), setHazelnut() )을 추가해야 합니다.

 

3. 또한, 새로운 음료가 출시될 때 특정 첨가물이 들어가지 않는 음료가 있을 것입니다. 들어가지 않는 첨가물의 인스턴스를 갖게 된다는 문제점이 있습니다.

 

4. 만약에 고객이 휘핑크림을 두 번 넣어달라고 하면 이를 처리할 수 없습니다. 메소드에는 휘핑크림이 있는지, 없는지만 판단하는 hasWhip() 메소드가 있기 때문입니다. : 중복된 첨가물에 대한 표현이 불가능

 

$\to$ 상속이 강력하기는 하지만, 상속을 사용한다고 해서 무조건 유연하거나 관리하기 쉬운 디자인이 되는 것이 아닙니다.

$\to$ 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있습니다.

$\to$ 객체를 동적으로 구성하면 기존 코드를 고치는 대신, 새로운 코드를 만들어서 기능을 추가할 수 있습니다.

 


Decorator Pattern 적용

위와 같은 코드의 문제점을 해결하기 위하여 Decorator Pattern을 적용해 보도록 하겠습니다.

 

특정 음료에서 시작하여 첨가물을 장식하는 방법입니다.

예를 들어, 고객이 모카에 휘핑크림을 추가한 다크 로스트 커피를 주문한다면 다음과 같이 장식할 수 있습니다.

 

1. DarkRoast 객체 생성

2. Mocha 객체로 장식

3. Whip 객체로 장식

4. cost() 메소드를 호출하여 가격을 계산. 이때, 첨가물의 가격을 계산하는 일은 해당 객체에게 위임.

 

 

 

 

다시 데코레이터 패턴을 정리해보도록 하겠습니다.

- 데코레이터 패으로 객체에 추가 요소를 동적으로 더할 수 있습니다.

- 데코레이터를 사용하면 서브클래스를 만들 때 보다 훨씬 유연하게 기능을 확장할 수 있습니다.

 

데코레이터의 클래스 다이어그램은 다음과 같은걸 위에서 확인했었습니다.

 

위의 다이어그램을 현재 Coffee house 예제에 적용해보도록 하겠습니다.

 

Component : Beverage

ConcreteComponent : Esspresso, Decaf, DarkRoast, HouseBlend

Decorator : CondimentDecorator (첨가물 데코레이터 Class)

ConcreteDecoratorA , B, ... : Mocha, Whip, Milk, Soy (첨가물 구상 Class)

 

 

Coffee house에 Decorator Pattern을 적용한 Class diagram은 위와 같습니다.

 

위의 Class diagram을 기반으로 구현한 JAVA 코드를 살펴보도록 하겠습니다.

 

Beverage class

  • abstrac Class이며 description을 인스턴스 변수로 가지고, getDescription(), cost() 메소드를 갖습니다.
  • getDescription()은 이미 구현되어 있지만, cost()는 abstract로 구현하여 서브클래스에서 구현해야 하는 것을 확인할 수 있습니다.

 

 

CondimentalDecorator

다음으로 첨가물을 나타내는 추상클래스 입니다.

  • Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하므로 Beverage 클래스를 확장합니다.
  • 모든 첨가물 데코레이터에 getDescription() 메소드를 새로 구현하도록 abstract 메소드인 getDescription()을 선언합니다.

 

 

ConcreteComponent : Esspresso, HouseBlend, Decaf, DarkRoast

다음으로 실제 음료 클래스인 Espresso 클래스입니다.

  • Beverage 클래스를 상속받습니다.
  • 클래스 생성자 부분에서 description을 설정합니다.
  • 음료 가격을 cost()메소드에서 return 합니다.

 

동일하게 HouseBlend 클래스입니다.

 

Decaf과 DarkRoast 클래스도 위와 같은 방법으로 구현하면 다음의 Class diagram을 얻게 됩니다.

 

Condiment Class : Mocha, Soy, Milk, Whip

마지막으로 첨가물 코드입니다.

  • 첨가물은 CondimentDecorator를 상속받습니다.
  • CondimentDecorator에 abstract로 선언된 getDescription() 메소드를 구현합니다.
  • cost() 메소드를 통해 첨가물의 가격을 기존 음료 가격에 더해줍니다. (beverage.cost() + .20;)

 

Mocha Class와 동일하게 Soy, Milk, Whip을 구현하고 나면 다음과 같은 클래스 다이어그램을 얻습니다.

 

 

모카가 2번, 우유, 휘핑크림이 첨가된 Espresso 커피를 제작하는 코드는 다음과 같습니다.

Beverage beverage = new Espresso();
beverage = new Mocha(beverage);
beverage = new Mocha(beverage);
beverage = new Whip(beverage);
beverage = new Milk(beverage);

 

1. Espresso 객체를 생성 $\to$ description = "에스프레소", cost = 1.99

2. 객체를 Mocha 클래스로 장식 $\to$ description = "에스프레소, 모카", cost = 2.19

3. 객체를 Mocha 클래스로 장식 $\to$ description = "에스프레소, 모카, 모카", cost = 2.39

4. 객체를 Whip 클래스로 장식 $\to$ description = "에스프레소, 모카, 모카, 휘핑크림", cost = 2.69

5. 객체를 Milk 클래스로 장식 $\to$ description = "에스프레소, 모카, 모카, 휘핑크림, 우유", cost = 2.99

 

 

이로써 Coffee house에 Decorator pattern 적용을 완료하였습니다.


만약 여기에 커피 사이즈별로 첨가물 가격을 다르게 받는다면 어떻게 코드를 구현해야 할까요?

제가 생각하는 클래스 다이어그램은 다음과 같습니다.

 

커피를 선택할 때 setSize() 메소드를 통해 사이즈를 선택하고, CondimentDecorator에서 getSize()를 통해 beverage.getSize()를 리턴합니다.

 

그 후 첨가물 class에서 if문을 활용하여 Size 별로 가격을 책정하는 코드를 사용하면 해결될 것 같습니다.

 

 

반응형