디자인 패턴/Design Pattern

[디자인 패턴] 장식자 패턴(Decorator Pattern)

JunsuKim 2022. 10. 14.
728x90

장식자 패턴(Decorator Pattern)이란?

  • 객체에 동적으로 새로운 행위를 추가할 수 있도록 해주는 구조 패턴이다.
  • 클래스의 책임을 실행 시간에 코드를 수정하지 않고 바꾸고 싶을 때 사용한다.
    • 전략 패턴도 이 기능이 가능하다.
    • 전략 패턴은 전략 객체를 바꿔 실행시간에 확장한다.
    • 장식자 패턴은 한 객체를 다른 객체로 포장하여 책임을 추가/변경한다
  • 장식한 개체는 여전히 원래 객체와 같은 타입이다. -> is-a
  • 한 객체를 여러 번 포장할 수 있고, 보통 포장하는 순서가 중요하지 않다.
    • 포장하는 순서가 중요할 시 제한을 따로 둬야한다.
  • is-a와 has-a를 모두 사용한다.
    • has-a(포함 관계)를 이용해 책임을 추가/변경한다.

사용하는 이유

위와 같은 주문 시스템이 있다고 하자. 주 목적은 가격을 계산하는 것이다.

커피에는 HouseBlend, DarkRoast, Decaf, Expresso와 같은 종류가 있다.

이때, 커피 주문 시 요구사항을 추가하고 싶다고 하자.

ex) 우유 좀 더 넣어주세요, 휘핑크림은 빼주세요 등등

이처럼 추가해야하는 상황이 많아지게 된다면, 클래스가 수도 없이 많아지게 된다.(클래스 폭발, class explosion)

이러한 문제를 해결하는 방법을 알아보자.

우선 다음과 같은 방법을 생각해낼 수 있다.

위와 같이 되면 첨가물에 대한 계산은 Beverage 클래스의 cost에서 계산한다.

하지만 다음과 같은 문제점이 있다.

  • 추가되는 것의 가격이 변동되면 기존 코드를 변경해야 한다.
  • 새로운 첨가물이 생기면 새 메소드를 추가해야 하며, cost 메소드 또한 수정해야 한다. 즉 OCP원칙에 위배된다.
  • 새로운 음료가 추가될 수 있고, 기존 첨가물이 새 음료에 적합하지 않을 수 있다. 즉 LSP, ISP 원칙에 위배된다.
    • 새 음료에 적합하지 않은 것은 장식 제한을 통해 해결할 수 있다.
  • 상속을 통해 행위를 상속받을 수 있지만, 상속되는 행위는 컴파일 시간에 고정되며, 모든 자식 클래스는 동일한 행위를 상속받아야 한다.
    • has-a를 이용하면 객체에 행위를 실행 시간에 추가할 수 있다.

위의 문제들을 해결할 수 있게 해주는 것이 장식자 패턴(Decorator Pattern)이다.

장식자 패턴(Decorator Pattern) 구현

커피 예제의 장식자 패턴을 구현한 것을 UML로 보면 위와 같다.

장식자 패턴의 구현을 위해서는 패턴의 구성 요소를 알아야한다.

  • Component(Beverage 클래스): 인터페이스 혹은 추상 클래스를 정의한다. 즉, 사용할 기본 틀이 된다.
  • ConcreateComponent(DarkRoast, HouseBlend): Component를 장식할 구체적인 객체 타입이다.
  • ComponentDecorator: 추상클래스 혹은 인터페이스이며, Component의 서브 클래스이다. Component와 ConcreateDecorator 사이에서 연결해주는 역할을 한다.
  • ConcreateDecorator(Milk, Mocha, Whip): 확장 및 추가할 기능을 작성한다.

장식자 패턴 코드

Component(Beverage)

public abstract class Beverage{
   private String description = "이름없는 음료";
   public void setDescription(String description){
      this.description = description;
   }
   public String getDescription(){
      return description;
   }
   public abstract int cost();
}

ConcreateComponent(DarkRoast, HouseBlend)

public class DarkRoast extends Beverage {
   public DarkRoast(){
      setDescription("다크로스트 커피");
   }
   @Override
   public int cost() {
      return 1200;
   }
}
public class HouseBlend extends Beverage {
   public HouseBlend(){
      setDescription("하우스블랜드 커피");
   }
   @Override
   public int cost() {
      return 1000;
   }
}

CondimentDecorator

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

ConcreateDecorator(Milk, Mocha, Whip)

public class Milk extends CondimentDecorator {
   private Beverage beverage;
   public Milk(Beverage beverage){
      this.beverage = beverage;
   }
   @Override
   public String getDescription() {
      return beverage.getDescription()+", 우유";
   }
   @Override
   public int cost() {
      return beverage.cost()+500;
   }
}
public class Mocha extends CondimentDecorator {
   private Beverage beverage;
   public Mocha(Beverage beverage){
      this.beverage = beverage;
   }
   @Override
   public String getDescription() {
      return beverage.getDescription()+", 모카";
   }
   @Override
   public int cost() {
      return beverage.cost()+200;
   }
}
public class Whip extends CondimentDecorator {
   private Beverage beverage;
   public Whip(Beverage beverage){
      this.beverage = beverage;
   }
   @Override
   public String getDescription() {
      return beverage.getDescription()+", 크림";
   }
   @Override
   public int cost() {
      return beverage.cost()+500;
   }
}

적용자 패턴을 적용할 수 있는 상황

  • 상속에 의한 제약사항에 구속받지 않고 다른 동일 타입 객체들에게 영향을 주지 않고 동적으로 객체에 책임을 추가하고 싶을 경우
  • 시스템이 동작하면서 객체에 기능을 추가하고 제거하고 싶을 경우
  • 동적으로 객체에 추가할 수 있는 서로 독립적인 다양한 기능들이 존재할 경우

장단점

  • 코딩이 단순해지며, 클래스의 응집력이 높아질 수 있다.
  • 상속보다 유연하게 객체에 기능 또는 상태를 추가할 수 있으며, 나중에 제거 또한 가능하다.
    • 장식을 통한 제거보다는 새롭게 장식하는 방법을 주로 사용한다.
  • 관련 클래스에 중복되어 있는 장식 요소를 한 곳에 정의할 수 있다.
  • 클래스가 비교적 많이 정의될 수 있고, 디버깅이 힘들어질 수 있다.
    • 장식자 패턴의 실제 목적은 클래스의 수를 줄이는 것이다.
728x90

댓글