본문 바로가기

ComputerScience/DesignPattern

[DesignPattern] Decorator Pattern

Decorator Pattern

 데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 동적 혹은 정적으로 어떤 객체에 책임을 덧붙이는 패턴으로,
기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있습니다.

여기서 동적으로 추가할 때는 보통 특정 객체를 결합하는 방식을 사용합니다.

 

 

이미지 출처: https://en.wikipedia.org/wiki/Decorator_pattern


Class Diagram (클래스 다이어그램)

Abstract Decorator (추상 데코레이터) 클래스

  • 이 클래스는 Component 객체를 참조하는 참조 변수 (component)를 유지합니다.
  • 모든 요청을 이 참조된 객체로 전달합니다 (component.operation()).
  • 이로써 Decorator는 Component의 클라이언트에게 투명하게(보이지 않게) 동작합니다.

Concrete Decorator (구체적인 데코레이터) 클래스 (Decorator1, Decorator2)

  • 이 클래스들은 부가적인 동작을 추가할 수 있도록 addBehavior() 메서드를 구현합니다.
  • 요청을 전달하기 전이나 후에 Component에 추가적인 동작을 수행합니다.

Sequence Diagram (순서 다이어그램)

Client 객체

  • Component1 객체의 기능을 확장하기 위해 Decorator1과 Decorator2 객체를 통해 작업합니다.
  • Decorator1에 있는 operation() 메서드를 호출합니다.

Decorator1 객체

  • operation() 메서드를 호출받아 해당 요청을 Decorator2로 전달합니다.
  • Decorator2에게 전달한 요청 이후, addBehavior()를 수행합니다.
  • Decorator2로부터 반환된 결과를 받아 다시 addBehavior()를 수행하고 Client에 반환합니다.

Decorator2 객체

  • 받은 요청을 Component1로 전달하고, addBehavior()를 수행합니다.
  • 수행 결과를 Decorator1에게 반환합니다.

Client 객체 

  • Decorator1으로부터 반환된 결과를 받아 addBehavior()를 수행하고 최종 결과를 얻습니다.

이러한 구조는 데코레이터 패턴을 사용하여 객체에 동적으로 새로운 책임(동작)을 추가하고, 이를 통해 객체의 기능을 확장하는 방법을 보여주고 있습니다. 이 패턴은 객체 간의 결합을 약화시키면서도 확장성을 제공합니다.

디자인 원칙 OCP(Open-Closed Principle)

클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.
이해하기 쉽게 말하면, 기존의 코드의 변경 없이 새로운 행동을 추가할 수 있어야 한다는 것입니다.

OCP 원칙을 모든 부분에서 준수하려고 한다면 쓸데없는 시간을 낭비할 수 있고,
필요 이상으로 복잡하고 이해하기 힘든 코드를 만들게 되는 부작용이 발생할 수 있습니다.
따라서 디자인한 것들 중 가장 바뀔 가능성이 높은 부분을 중점적으로 살피고, OCP를 적용하는 것이 좋습니다.

데코레이터 패턴 예시

특정 커피의 가격이 얼마인지 출력하는 프로그램을 만들고 싶다고 가정해 봅시다.

만약 여기서 휘핑크림을 추가한 가격을 계산해야 한다거나 하면 어떻게 디자인할 수 있을까요?

데코레이터 패턴을 추가하지 않는다면, Beverage에 setWhip(), hasWhip() 등의 메서드를 넣고 cost()에 추가해야 할 것입니다.

그러나 새로운 커피가 추가될 때 휘핑크림을 추가할 수 없는 커피가 나온다거나 하면, hasWhip() 등의 메서드가 여전히 상속받게 되고
새로운 재료의 추가에 따라 Beverage의 메서드가 무한정 많아지게 될 것입니다.

 

이때 Decorator를 상속받는 클래스를 구현하면 됩니다. (OCP, 확장되어도 기존의 코드에는 변경이 없다.)

Beverage

public abstract class Beverage {
    String description = "제목 없음";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}

public class Espresso extends Beverage {
    public Espresso() {
        description = "에스프레소";
    }

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

 

Decorator

public abstract class CondimentDecorator extends Beverage {
    Beverage beverage;

    public abstract String getDescription();
}

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

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

    public double cost() {
        return beverage.cost() + .10;
    }
}

 

main code

public class Application {
    public static void main(String[] args) {
        Beverage beverage = new HouseBlend();
        beverage = new Mocha(beverage);
        beverage = new Mocha(beverage);
        beverage = new Whip(beverage);

        System.out.println(beverage.getDescription() + " $ " + beverage.cost());
    }
}

 

Output

하우스 블렌드 커피, 모카, 모카, 휘핑 $ 1.3900000000000001

 

고려할 점과 장단점

장점

 

데코레이터 패턴의 장점은 이미 여러 가지로 분리되어 있는 커피를 상위 클래스에 추가로 뭔가를 작성하는 것이 아니라,
데코레이터를 이어 붙여서 추가 기능을 핵심 기능과 분리할 수 있다는 것입니다.
상속 대신 구성과 위임으로 동적으로 새로운 행동을 추가할 수 있습니다.

 

단점 및 고려해야 하는 점

 

만약, 구상 구성 요소(ConcreteComponent, 위 예시에서는 Espresso)에서 메서드를 통해 특별 할인 행사를 한다거나 하는 작업을 한다고 하면 어떨까요?


데코레이터로 감싸지면 구상 구성 요소로 어떤 작업을 처리하는 코드는 제대로 작동하지 않을 수 있습니다.
즉, 구상 구성 요소로 돌아가는 코드를 만들어야 한다면 데코레이터 패턴 사용을 다시 한번 생각해보아야 합니다.


또한 데코레이터 패턴을 쓰면 관리해야 할 객체가 늘어나 코딩할 때 실수할 가능성이 높아져 실제로는 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용합니다.

 

그리고 코드가 복잡해질 수 있습니다.

 

참고 자료

https://github.com/IT-Book-Organization/HeadFirst-DesignPattern/blob/main/Chapter_03/README.md

https://en.wikipedia.org/wiki/Decorator_pattern