소프트웨어 관련/Design Pattern

[Design Pattern] Decorator Pattern

JJangGu 2022. 4. 7. 01:46

헤드퍼스트의 Design Pattern 중 세번째, Decorator Pattern 입니다.

 

Decorator Pattern

객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

 

Decorator Pattern 에 대한 정의입니다. 하지만 이 내용을 보면 코드가 어떻게 생겼는지 감을 잡기가 어렵습니다. 책의 예제를 보면서 패턴을 설명해보겠습니다. 

먼저, 패턴이 적용되지 않은 예제에서 문제점을 살펴보겠습니다.

 

위의 설계에서 Beverage는 음료를 나타내는 추상 클래스이고, 커피샵에서 판매되는 모든 음료는 이 클래스의 서브 클래스가 됩니다.

위의 설계에서는 메뉴에 우유, 모카, 휘핑 크림 등이 추가할때마다 커피 가격이 올라가기 때문에 Beverage 를 상속받는 서브 클래스를 계속해서 만들어나가면서 확장을 했습니다. ( ex. HouseBlendWithSteamMilk, HouseBlendWithSteamMilkAndMoca ..... )

 

이와 같은 문제에서 한번 개선을 한 설계를 보겠습니다. 

이제 추가 요소에 해당하는 부분을 변수로 만들고, 슈퍼 클래스에 있는 cost() 에서는 추가된 각 항목의 가격을 계산합니다. 서브클래스에서 cost() 메서드를 오버라이드 할 때는 기능을 확장하여 특정 음료 형식의 가격을 더하는 구조입니다. 

이러면 위에서 만든 HouseBlendWithSteamMilk, HouseBlendWithSteamMilkAndMoca .....  이런 많은 클래스를 만들지 않아도 되겠죠. 하지만 이런 설계도 문제가 있습니다. 

 

우리는 Strategy Pattern 을 공부하면서 상속보다 구성을 활용해야 된다 라는 디자인 원칙을 알고 있습니다. 위와 같은 설계에서 만일 새로운 음료가 추가되었을때 불필요한 메서드를 상속받을 수 있습니다. Tea 에 hasWhip() 을 가지는 것은 이상한 일이죠. 가격을 합산해야 하는데 손님이 휘핑크림을 두번 추가했다면 어떨까요?? 위의 설계로는 한계가 있습니다.

 

즉, 위의 설계에는 확장이 어렵습니다. 객체 지향 설계를 공부하신 분은 여기서 원칙하나를 생각하실 수 있을겁니다.

OCP(Open-Closed Principle) 이죠. 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.

 

이제 Decorator Pattern 이 어떻게 이 원칙들을 지키면서 확장을 용이하게 하는지 살펴봅시다.

만일 손님이 DarkRoast 커피에 모카와 휘핑을 추가한다고 가정해봅시다.

 

우리는 이와 같은 순서로 주문을 처리할 것 입니다. 

  • 1. DarkRoast 객체를 가져온다

  • 2. Mocha 객체로 장식한다.

  • 3. Whip 객체로 장식한다.

  • 4. cost() 메서드를 호출한다. 이때 첨가물의 가격을 계싼하는 일은 해당 객체들에 위임한다.

 

이런 과정을 통해서 한 객체를 여러개의 데코레이터로 감싸고, 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업도 수행할 수 있습니다.

 

Decorator Pattern 을 클래스 다이어그램으로 표현하면 이런 모양이 됩니다. 

이 모양을 잘 기억하면서 다시 커피숍의 다이어그램을 그려보겠습니다.

 

 

처음의 설계와는 많이 달라진 모습입니다. 그러면 코드를 보기전에 이게 어떻게 구성을 활용한 설계인지 살펴보겠습니다. CobdimentDecorator 에서 Beverage 를 상속을 통해서 확장하고 있습니다. 그러나 여기서는 상속을 통해서 행동을 물려받는 것이 아닌 형식을 맞추는 것에 초점을 두고 있습니다. 데코레이터 객체가 자신이 감싸고 있는 객체랑 같은 인터페이스를 가짐으로써 원래 있던 구성요소가 들어갈 자리에 자신이 들어갈 수 있는 것이죠. 

 

어떤 구성요소를 가지고 데코레이터를 만들 때 새로운 행동을 추가하게 되면 이 새로운 행동은 슈퍼 클래스로 부터의 상속이 아닌, 객체를 구성하는 방법을 통해서 얻게 되는 것이죠. 

 

마지막으로 구현된 코드를 보며 마무리 하겠습니다.

 

먼저, Beverage 입니다.

public abstract class Beverage {
   String description = "";

   public String getDescription() {
      return this.description;
   }

   public abstract double cost();
}

 

다음으로는 CondimentDecorator 입니다.

public abstract class CondimentDecorator extends Beverage{
   public abstract String getDescription();
}

 

Beverage 를 상속받은 Espresso 와 HouseBlend 입니다.

public class Espresso extends Beverage{

   public Espresso() {
      description = "Espresso";
   }

   @Override
   public double cost() {
      return 1.99;
   }
}

 

public class HouseBlend extends Beverage{

   public HouseBlend() {
      description = "House Blend";
   }

   @Override
   public double cost() {
      return 1.99;
   }
}

 

CondimentDecorator 를 상속받은 Mocha 와 Whip 입니다.

public class Mocha extends CondimentDecorator {
   Beverage beverage;

   public Mocha(Beverage beverage) {
      this.beverage = beverage;
   }

   @Override
   public String getDescription() {
      return beverage.getDescription() + ", Mocha";
   }

   @Override
   public double cost() {
      return 0.20 + beverage.cost();
   }
}

 

public class Whip extends CondimentDecorator{
   Beverage beverage;

   public Whip(Beverage beverage) {
      this.beverage = beverage;
   }

   @Override
   public String getDescription() {
      return beverage.getDescription() + ", Whip";
   }

   @Override
   public double cost() {
      return 0.10 + beverage.cost();
   }
}

 

이제 코드를 실행해보겠습니다.

public class CoffeeShop {
   public static void main(String[] args) {
      Beverage beverage = new Espresso();
      System.out.println(beverage.getDescription() + " $" + beverage.cost());

      Beverage beverage2 = new HouseBlend();
      beverage2 = new Mocha(beverage2);
      beverage2 = new Mocha(beverage2);
      beverage2 = new Whip(beverage2);
      System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
   }
}

결과입니다.

 

Decorator Pattern 에는 한계점도 존재합니다. 만일 구성요소의 형식을 알아내서 그 결과를 바탕으로 어떤 작업을 처리하는 코드에 이 패턴을 적용하면 제대로 동작하지 않습니다. 추상 구성요소의 형식을 바탕으로 돌아가는 코드에 대해서 패턴을 적용해야만 제대로 된 결과를 얻을 수 있습니다. 예를 들어 HouseBlend 커피는 할인을 한다고 했을 때, 데코레이터가 감싸고 나면 그 커피가 HouseBlend 인지 아닌지 판단하기가 어렵죠.

 

또한 Decorator Pattern을 쓰면 관리해야할 객체가 늘어나고, 실수할 가능성도 높아집니다. 하지만 일반적으로 팩토리나 빌더와 같은 다른 패턴을 써서 만들고 사용하게 되므로 뒤에 이 패턴들을 공부 한 후에 다시 보면 도움이 될 거라고 생각됩니다.