봉황대 in CS

[디자인 패턴] 데코레이터 패턴은 객체에 행동을 '추가'하는 용도로만 사용 가능하다 본문

Computer Science & Engineering/Design Pattern

[디자인 패턴] 데코레이터 패턴은 객체에 행동을 '추가'하는 용도로만 사용 가능하다

등 긁는 봉황대 2023. 1. 5. 16:35

* 본 글은 '헤드퍼스트 디자인패턴(Head First Design Patterns) 개정판'의 내용을 참고하여 작성하였습니다.

 

Chapter 3. 데코레이터 패턴에서의 예제 코드를 보며,

궁금증이 생겨 시도를 한 과정과 그것을 통해 깨달은 바들을 여기에 작성하려 한다.

 

Decorator Pattern


데코레이터 패턴이 무엇인지에 대해서는 다음의 링크들을 참고하길 바란다.

 

1. 김베어씨 정리

 

GitHub - IT-Book-Organization/HeadFirst-DesignPattern

Contribute to IT-Book-Organization/HeadFirst-DesignPattern development by creating an account on GitHub.

github.com

 

2. 내 정리 (스포 존재)

 

Chapter 3. 데코레이터 패턴

객체 꾸미기, 객체 작성이라는 형식으로 실행 중에 클래스를 꾸미는(데코레이션하는) 방법

www.notion.so

 

 

기존 예제 코드들


main

public class StarbuzzCoffee {

    public static void main(String[] args) {
        Beverage beverage1 = new Espresso();
        System.out.println(beverage1.getDescription() + " $" + String.format("%.2f", beverage1.cost()));

        Beverage beverage2 = new DarkRoast();
        beverage2 = new Mocha(beverage2);
        beverage2 = new Mocha(beverage2);
        beverage2 = new Whip(beverage2);
        System.out.println(beverage2.getDescription() + " $" + String.format("%.2f", beverage2.cost()));

        Beverage beverage3 = new HouseBlend();
        beverage3.setSize(Beverage.Size.VENTI);
        beverage3 = new Soy(beverage3);
        beverage3 = new Mocha(beverage3);
        beverage3 = new Whip(beverage3);
        System.out.println(beverage3.getDescription() + " $" + String.format("%.2f", beverage3.cost()));

        Beverage beverage4 = new HouseBlend();
        beverage4 = new Soy(beverage4);
        beverage4 = new Mocha(beverage4);
        beverage4 = new Whip(beverage4);
        beverage4.setSize(Beverage.Size.VENTI);
        System.out.println(beverage4.getDescription() + " $" + String.format("%.2f", beverage4.cost()));
    }

}

 

추상 클래스 Beverage

public abstract class Beverage {

    public enum Size {TALL, GRANDE, VENTI}

    Size size = Size.TALL;

    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }

    public Size getSize() {
        return size;
    }

    public void setSize(Size size) {
        this.size = size;
    }

    public abstract double cost();

}

 

추상 클래스 CondimentDecorator

위에서의 클래스 Beverage를 확장한다.

public abstract class CondimentDecorator extends Beverage {
    // extends Beverage : Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하기 때문

    Beverage beverage; // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체

    public abstract String getDescription();

    public Size getSize() {
        return beverage.getSize();
    }

    public void setSize(Size size) {
        beverage.setSize(size);
    }

}

 

데코레이터 객체 (e.g., Soy)

추상 클래스 CondimentDecorator의 구현체들이다.

Soy는 음료 사이즈에 따라 가격이 바뀐다.

public class Soy extends CondimentDecorator {

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

    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }

    public double cost() {
        double cost = beverage.cost();
        if (beverage.getSize() == Size.TALL) {
            cost += .10;
        } else if (beverage.getSize() == Size.GRANDE) {
            cost += .15;
        } else if (beverage.getSize() == Size.VENTI) {
            cost += .20;
        }
        return cost;
    }

}

 

 

 

궁금즘과 시도


음료 사이즈를 설정하는 타이밍에 따라 가격 계산 결과가 바뀔까??

→ 바뀌지 않는 것이 옳다고 생각

 

이를 알아보기 위해서 main 함수의 코드를 다음과 같이 수정해보았다.

public static void main(String[] args) {

    // 중략

    Beverage beverage3 = new HouseBlend();
    beverage3.setSize(Beverage.Size.VENTI);  // !!
    beverage3 = new Soy(beverage3);  // Soy 선언 부분
    beverage3 = new Mocha(beverage3);
    beverage3 = new Whip(beverage3);
    System.out.println("\\n음료 사이즈를 먼저 설정한 경우");
    System.out.println(beverage3.getDescription() + " $" + String.format("%.2f", beverage3.cost()));

    Beverage beverage4 = new HouseBlend();
    beverage4 = new Soy(beverage4);  // Soy 선언 부분
    beverage4 = new Mocha(beverage4);
    beverage4 = new Whip(beverage4);
    beverage4.setSize(Beverage.Size.VENTI);  // !!
    System.out.println("\\n음료 사이즈를 나중에 설정한 경우");
    System.out.println(beverage4.getDescription() + " $" + String.format("%.2f", beverage4.cost()));

}

 

결과

음료 사이즈를 먼저 설정한 경우
House Blend Coffee, Soy, Mocha, Whip $1.39

음료 사이즈를 나중에 설정한 경우
House Blend Coffee, Soy, Mocha, Whip $1.29

 

 

왜 두 가격이 다르게 나왔을까?? (나의 예상)


아래 그림처럼 데코레이터들은 객체를 감싸는 형태로 이루어지며,

cost 계산 시 먼저 가장 안쪽으로 들어간 후 재귀적으로 계산이 진행된다.

 

 

1. 음료 사이즈를 먼저 설정한 경우

음료 사이즈의 영향을 받는 Soy를 선언하기 이전에, 음료 사이즈를 먼저 설정했기 때문에 VENTI 사이즈가 Soy에 적용된 것이다.

(즉, 음료 사이즈 설정 이후 선언한 Soy, Mocha, Whip은 모두 VENTI 사이즈로 적용됨)

 

2. 음료 사이즈를 나중에 설정한 경우

Soy 선언 이후 음료 사이즈를 설정했기 때문에 Soy에는 VENTI 사이즈가 적용되지 않았다.

(음료 사이즈 설정 이후 선언한 데코페이터들에만 적용될 것)

따라서 맨 마지막에 선언한 Whip에만 VENTI 사이즈가 적용되었을 것이다. → 검증해보자

 

(취소선은 틀린 부분을 뜻함)

 

 

검증 과정


맨 마지막에 선언한 객체에 대해서만 사이즈가 적용되었을 것임을 검증해보기 위해서, 코드를 아래와 같이 추가하게 되었다.

 

데코레이터 객체

각 데코레이터 객체들에 cost 메서드가 호출되었을 때 음료의 사이즈를 출력하는 코드를 추가하였다.

public class Soy extends CondimentDecorator {

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

    public String getDescription() {
        return beverage.getDescription() + ", Soy";
    }

    public double cost() {
        System.out.println("[ Soy ] beverage size: " + beverage.getSize());  // 음료의 사이즈 출력
        double cost = beverage.cost();
        if (beverage.getSize() == Size.TALL) {
            cost += .10;
        } else if (beverage.getSize() == Size.GRANDE) {
            cost += .15;
        } else if (beverage.getSize() == Size.VENTI) {
            cost += .20;
        }
        return cost;
    }

}

 

main

음료 사이즈를 나중에 설정한 후 새로운 데코레이터를 선언하는 경우도 아래에 추가했다.

public static void main(String[] args) {
    
	// 중략

    System.out.println("\\n음료 사이즈를 먼저 설정한 경우");
    Beverage beverage3 = new HouseBlend();
    System.out.println("beverage5 size (사이즈 설정 전): " + beverage3.getSize());
    beverage3.setSize(Beverage.Size.VENTI);
    System.out.println("beverage5 size (사이즈 설정 후): " + beverage3.getSize());
    beverage3 = new Soy(beverage3);
    beverage3 = new Mocha(beverage3);
    beverage3 = new Whip(beverage3);
    System.out.println(beverage3.getDescription() + " $" + String.format("%.2f", beverage3.cost()));

    System.out.println("\\n음료 사이즈를 나중에 설정한 경우");
    Beverage beverage4 = new HouseBlend();
    beverage4 = new Soy(beverage4);
    beverage4 = new Mocha(beverage4);
    beverage4 = new Whip(beverage4);
    beverage4.setSize(Beverage.Size.VENTI);
    System.out.println(beverage4.getDescription() + " $" + String.format("%.2f", beverage4.cost()));

    System.out.println("\\n음료 사이즈를 나중에 설정하고, 그 다음에 우유를 선언한 경우");
    Beverage beverage5 = new HouseBlend();
    beverage5 = new Soy(beverage5);
    beverage5 = new Mocha(beverage5);
    beverage5 = new Whip(beverage5);
    System.out.println("beverage5 size (사이즈 설정 전): " + beverage5.getSize());
    beverage5.setSize(Beverage.Size.VENTI);  // !!
    System.out.println("beverage5 size (사이즈 설정 후): " + beverage5.getSize());
    beverage5 = new Milk(beverage5);
    System.out.println(beverage5.getDescription() + " $" + String.format("%.2f", beverage5.cost()));

}

 

결과

음료 사이즈를 먼저 설정한 경우
beverage3 size (사이즈 설정 전): TALL
beverage3 size (사이즈 설정 후): VENTI
[ Whip ] beverage size: VENTI
[ Mocha ] beverage size: VENTI
[ Soy ] beverage size: VENTI
House Blend Coffee, Soy, Mocha, Whip $1.39

음료 사이즈를 나중에 설정한 경우
[ Whip ] beverage size: TALL
[ Mocha ] beverage size: TALL
[ Soy ] beverage size: TALL
House Blend Coffee, Soy, Mocha, Whip $1.29

음료 사이즈를 나중에 설정하고, 그 다음에 우유를 선언한 경우
beverage5 size (사이즈 설정 전): TALL
beverage5 size (사이즈 설정 후): TALL
[ Milk ] beverage size: TALL
[ Whip ] beverage size: TALL
[ Mocha ] beverage size: TALL
[ Soy ] beverage size: TALL
House Blend Coffee, Soy, Mocha, Whip, Milk $1.39

 

띠요옹

나의 예상대로였다면 마지막에 '[ Milk ] beverage size: VENTI'였어야 했다.

 

 

데코레이터 객체들의 구조를 그려보자


'(3) 음료 사이즈를 나중에 설정한 후 새로운 데코레이터를 선언한 경우'의 객체 구조는 다음과 같이 예상할 수 있다.

 

 

 

왜 이런 결과가 나오게 되었을까?


beverage.getSize()는 추가된 데코레이터에 대한 것이 아닌, 그 중앙이 되는 객체에 대한 값으로 받아오게 된다.

 

 

왜일까??

cost()처럼, getSize()도 재귀적으로 받아오게 되기 때문이라는 결론을 내릴 수 있었다!!

 

 

따라서 아래와 같이 CondimentDecoratorsetSize()에 대한 함수를 추가하니,

음료 사이즈를 먼저 설정하든, 나중에 설정하는 동일한 결과가 출력된다.

package Chapter_3.Condiments;

import Chapter_3.Beverages.Beverage;

public abstract class CondimentDecorator extends Beverage {
    // extends Beverage : Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하기 때문

    Beverage beverage;  // 각 데코레이터가 감쌀 음료를 나타내는 Beverage 객체

    public abstract String getDescription();

    public Size getSize() {
        return beverage.getSize();
    }

    // 추가된 부분!!
    public void setSize(Size size) {
       beverage.setSize(size);
    }

}

 

결과

음료 사이즈를 먼저 설정한 경우
beverage3 size (사이즈 설정 전): TALL
beverage3 size (사이즈 설정 후): VENTI
[ Whip ] beverage size: VENTI
[ Mocha ] beverage size: VENTI
[ Soy ] beverage size: VENTI
House Blend Coffee, Soy, Mocha, Whip $1.39

음료 사이즈를 나중에 설정한 경우
[ Whip ] beverage size: VENTI
[ Mocha ] beverage size: VENTI
[ Soy ] beverage size: VENTI
House Blend Coffee, Soy, Mocha, Whip $1.39

음료 사이즈를 나중에 설정하고, 그 다음에 우유를 선언한 경우
beverage5 size (사이즈 설정 전): TALL
beverage5 size (사이즈 설정 후): VENTI
[ Milk ] beverage size: VENTI
[ Whip ] beverage size: VENTI
[ Mocha ] beverage size: VENTI
[ Soy ] beverage size: VENTI
House Blend Coffee, Soy, Mocha, Whip, Milk $1.49

 

 

정리


위처럼 CondimentDecorator의 코드를 바꾸지 않는다면

beverage.setSize()는 바로 직전에 선언한 객체(또는 데코레이터 객체)에만 적용된다.

 

→ 이처럼 음료 사이즈에 대한 설정이 전체 객체들에 대하여 적용되어야 하는 상황이라면 설정 순서를 유의해야 한다.

 

 

1. 음료 사이즈를 먼저 설정한 경우

 

✅ 중앙 객체의 음료 사이즈를 먼저 설정해놓았기 때문에

cost() 계산 시 모든 객체에서 beverage.getSize()가 해당 사이즈로 받아와진다.

 

즉, 중앙 객체의 음료 사이즈가 VENTI로 설정되었으며

beverage.getSize()를 호출했을 때 최종적으로 VENTI로 불러와지므로

음료 사이즈 설정 이후 선언한 Soy, Mocha, Whip은 모두 VENTI 사이즈가 적용되었던 것이다.

 

 

2. 음료 사이즈를 나중에 설정한 경우

 

중앙 객체가 아닌, 데코레이터 객체에 대한 사이즈가 설정된 것이다.

 

즉, 중앙 객체는 계속 default 사이즈 값인 TALL로 설정되어 있고

beverage.setSize(VENTI)는 데코레이터 객체 Whip에 대해서 사이즈가 설정된 것이기 때문에

데코레이터 객체에서 beverage.getSize()를 호출했을 때 최종적으로 TALL로 불러와지므로 모두 TALL 사이즈가 적용되었던 것이다.

 

 

결론: 데코레이터 패턴은 객체에 행동을 추가하는 용도로만 사용 가능하다


데코레이터는 감싸고 있는 객체에 행동을 추가하는 용도로 만들어진다.

 

→ 위 과정을 통해 이 말이 직접 와닿는 듯하다.

 

 

그렇다면 각 데코레이터가 각각 설정된 값에 따라 다른 행위를 취해야 하는 상황에서는 어떻게 구현해야 하는가??

(위 예시에서 Mocha까지는 TALL, Whip 이후부터는 VENTI로 설정되었던 것처럼 다르게 설정되는 상황)

 

이 때문에 특정 형식에 의존하는 클라이언트 코드에 데코레이터를 그냥 적용하면 절대 안 된다는 것인가?

 

책에서는 만약 어떤 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 한다면

원래 데코레이터 패턴이 만들어진 의도에 어긋난다고 작성되어 있다.

 

 

아무튼, 최종 정리하자면 다음과 같다.

 

1. 어떤 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야 하는 경우, 데코레이터 패턴을 사용하면 안 된다.

2. 데코레이터 패턴은 객체에 행동을 추가하는 용도로만 사용 가능하다.

 

 

반응형

'Computer Science & Engineering > Design Pattern' 카테고리의 다른 글

[디자인 패턴] 디자인 원칙  (0) 2023.01.04
Comments