본문 바로가기
디자인 패턴

7. Decorator Pattern

by spaul 2023. 11. 28.

▣ What is Decorator Pattern?

 

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

 

데코레이터 패턴은 객체에 추가적인 책임을 동적으로 부여하여, 상속을 사용하지 않고도 유연하고 융통성있는 기능 확장을 가능하게 하는 패턴입니다.

 

 즉, 우리가 클래스를 설계하다보면, 상속 관계를 정의하기가 어렵거나 너무 많은 상속 관계가 발생하는 경우가 있을 수 있는데, 데코레이터 패턴은 이런 문제를 해결하기 위해서 상속이 아닌 연관을 통해 Runtime에 필요한 기능을 추가할 수 있도록 하는 패턴입니다.

 

데코레이터 패턴 클래스 다이어그램

 

ConcreteDecorator 클래스에는 각 Component를 장식하기 위한 인스턴스 변수가 존재합니다. Component 클래스 보통 Abstract class 또는 Interface로 구현되어  ConcreteComponent에서 사용되거나 또는 Decorator로 감싸져서 사용됩니다. 또한 데코레이터 패턴에서의 상속은 기능을 물려받기 위한 것이 아닌, 형식을 맞추기 위함입니다. 디자인 패턴은 정의만 봐서는 이해가 잘 안됩니다. 아래에서 예시와 함께 살펴보겠습니다.

 

우리가 StarBuz(스타벅스가 아닙니다!)라는 카페의 사장이 되어 운영한다고 가정해보겠습니다. 초기에 아래 그림과 같이 4개의 메뉴가 있었습니다. 

 

 그런데 우리의 카페가 너무 장사가 잘되고 손님들의 니즈도 다양해져서, 다양한 메뉴를 추가하여 운영하려고 합니다. 메뉴를 하나 추가할 때마다 Beverage를 상속받는 클래스를 하나 만든다고 생각하면, 50가지의 메뉴를 추가하면 50개의 상속이 발생하게 됩니다. 이것이 위에서 말씀드린 '너무 많은 상속 관계가 발생하는 경우'입니다. 이처럼 불필요한 상속을 최대한 줄이기 위해서는 어떻게 해야될까요? 이를 해결하기 위해 데코레이터 패턴을 사용할 수 있습니다.

 

 더 쉬운 예시로 우리가 카페에서 음료를 주문하는 경우를 생가해볼까요? 예를 들어 DarkRoast원두를 사용한 휘핑크림이 올라간 아이스 모카 라떼를 먹으려고 하는데, 그냥 우유 대신 저지방 우유를 먹으려고 합니다. 그러면 우리는 메뉴판에서 우리가 주문하려는 그대로 '다크로스트 원두를 사용한 일반 우유대신 저지방 우유가 첨가된 휘핑크림이 올라간 아이스 모카 라떼'를 찾나요? 그런 사람은 아무도 없을 겁니다. 우리는 당연히 메뉴판에서 '모카라떼'라는 것을 먼저 찾을 것이고, 그 다크로스트 원두를 선택하고, 다음으로 ICE or HOT을 선택하고, 다시 거기서 휘핑크림 유무를 선택하고, 마지막으로 저지방우유 옵션을 선택할 것입니다.

 

 데코레이터 패턴을 사용하는 이유도 마찬가지입니다. 우리가 카페의 모든 옵션을 하나의 상품에 포함할 수 없는 것처럼, 하나의 메뉴를 두고 거기에 다양한 옵션을 추가하는, 마치 장식(데코레이트)과 같은 역할을 해주는 것입니다. 이를 코드로 구현해보겠습니다.

 

public abstract class Beverage {
    protected String description = "noTitle";

    public String getDescription() {
        return description;
    }
    public abstract double cost();
}

 

가장 기본이 되는 Beverage클래스입니다. 여기에 다양한 옵션들을 추가하여 우리가 원하는 음료를 주문해봅시다.

 

public class DarkRoast extends Beverage {

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

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

 

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

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

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

 

public class Ice extends Beverage {
    private Beverage beverage;

    public Ice(Beverage beverage) {
        this.beverage = beverage;
    }
    @Override
    public double cost() {
        return beverage.cost() + .10;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Ice";
    }
}
public class LowFatMilk extends Beverage {
    private Beverage beverage;

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

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

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

 

public class Whip extends CondimentDecorator {
    private Beverage beverage;

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

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

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

 

우리가 선택하려는 옵션들을 모두 추가한 음료를 한 번 주문해볼까요?

public class Main {
    public static void main(String[] args) {
        Beverage b1 = new DarkRoast();
        b1 = new Mocha(b1);
        b1 = new Ice(b1);
        b1 = new Whip(b1);
        b1 = new LowFatMilk(b1);
        System.out.println(b1.getDescription() + " $" + b1.cost());
    }
}

 

메인 함수에서 위와 같이 주문하면 

주문 결과

이런 출력 결과가 나오는 것을 볼 수 있습니다. 우리가 원하는대로 정확하게 잘 출력 되었군요! 만약 다른 옵션을 추가하고 싶다면, 해당되는 클래스를 만든 뒤에 위와 동일한 방식으로 추가해주면 됩니다. 우리가 주문한 최종 음료 객체의 모양은 아래 그림과 같게 됩니다.

 

 

우리가 처음에 만든 DarkRoast 인스턴스로부터 계속 Mocha, Ice, Whip, LowFatMilk 등을 추가하여 위와 같은 구조를 가지게 됩니다. b1의 cost()를 호출하면 LowFatMilk의 cost()가 다시 Whip의 cost()를 호출하고, Whip의 cost()가 다시 Ice의 cost()를 호출하고... 위와 같은 마치 재귀적인 함수 호출이 반복되어 결국 모든 옵션이 더해진 최종 음료 가격이 산출되게 됩니다.

 

 이것으로 데코레이터 패턴에 대해 알아보았습니다. 디자인 패턴은 정의가 어렵지, 예제를 통해 알고보면 막상 그렇게 어렵지는 않은 것 같습니다. 여러분도 저와 같은 느낌을 받으셨으면 좋겠습니다.

 

References

[1] 조용주, 고급객체지향프로그래밍

[2] Eric Freeman, Head Frist Design Patterns

'디자인 패턴' 카테고리의 다른 글

9. Builder Pattern  (0) 2023.12.05
8. Factory Method Pattern, Abstract Factory Patter  (0) 2023.12.02
6. Iterator Pattern  (0) 2023.11.14
5. Singleton Pattern  (0) 2023.11.13
4. Observer Pattern  (0) 2023.11.07