[CHAPTER 6] Decorator Pattern - review
이번 주부터는 [번외] 로 작성되는 리뷰 입니다.
스터디에서는 가장 많이 활용할 법한 패턴들을 다뤘지만,
지금부터는 그외에 적어도 "기본서에 서술된 패턴" 들에 대한 이야기를 다뤄볼 생각입니다.
고작, 6개의 패턴정도만 다루고 이 카테고리를 끝내기에는 조금 아쉽더군요.....
아마 이 것이 More OOP & FP....
참고로 여기서 기본서란 아래 책을 말합니다.
|
번외로 시작하는 이번 포스팅에서 처음 다뤄볼 패턴은 "주어진 상황 및 용도에 따라 객체에 책임을 덧붙일 수 있는 Decorator Pattern" 입니다.
이 패턴을 사용하면 동적으로 기능을 확장할 수 있으며, 클래스의 기능을 확장 하려면 꼭 상속을 해야 한다는 법칙을 대신할 수 있는 대안이 될 수 있을 것 같습니다.
한번, 요구사항과 함께 본격적으로 리뷰를 해보겠습니다.
1. Coffee 의 가격문제.
우리가 마시는 Coffee 에는 다양한 재료가 포함될 수 있습니다.
예를들어, Latte 에는 Espresso 와 Milk 가 포함되며, Mocha 에는 Latte 재료에 Chocolate Syrup 이 포함됩니다.
또한, Coffee 종류에 따라 Espresso 역시 다른 블랜딩 방식을 사용할 수 있을 것 같습니다.
이렇게 Coffee 마다 사용하는 재료는 다양하며,
그에 따라 가격을 책정하고 싶은 것이 이번 포스팅의 최종 목표입니다.
2. 상속관계를 통한 Coffee 가격 계산.
요구사항은 그렇게 큰 문제는 아닌 것처럼 보입니다.
Coffee 라는 최상위 클래스를 만들고 해당 클래스에서는 Coffee::getCost 라는 행위에 대한 서명을 명시합시다.
물론 구체적인 구현은 하위 클래스들이 할 일이죠..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | /** * 커피 클래스 * * <pre> * 계산에 대한 기능을 명시. * </pre> * * Created by Doohyun on 2017. 6. 25.. */ public abstract class Coffee { /** * 커피 이름 출력 * * @return */ public abstract String getName(); /** * 코스트에 대한 메소드 서명 정의 * * <pre> * 해당 클래스를 상속받는 클래스는 모두 이 기능을 구현해야함. * </pre> * * @return */ public abstract Integer getCost(); } /** * 라떼 기능 구현. * * Created by Doohyun on 2017. 6. 25.. */ public class Latte extends Coffee{ @Override public String getName() { return "라떼"; } /** * 라떼의 가격은 5900 원. * * @return */ @Override public Integer getCost() { return 5900; } } | cs |
이렇게 제품마다 가격에 대한 기능을 적절하게 구현해준다면, 요구사항을 쉽게 정리할 수 있을 것 같습니다.
이를 UML 로 표현해보면 다음과 같을 것 같군요.
일단, 제품에 따라 가격이란 개념을 표현할 수는 있습니다.
하지만 요구사항에서는 재료따라 Coffee 의 가격이 책정되길 바랍니다.
이는 즉 재료의 가격이 변하면 Coffee 의 가격도 변하길 바람을 말하는 듯 합니다.
3. 재료기능에 대한 기능 추가 및 상속구조 변화.
재료에 대한 기능을 상위 클래스에 정의를 함으로써, 이 문제를 해결할 수 있을 것 같습니다.
기본적인 커피 가격을 부모클래스에서 책정하고, 사용하는 재료에 따라 하위클래스에서 적절하게 가격을 추가하는 것이죠.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | /** * 커피 클래스 * * <pre> * 계산에 대한 기능을 명시. * </pre> * * Created by Doohyun on 2017. 6. 25.. */ public abstract class Coffee { /** * 우유 사용여부 * * @return */ public abstract Boolean getHasMilkYn(); /** * 코스트에 대한 메소드 서명 정의 * * <pre> * 기본적인 커피가격. * </pre> * * @return */ public Integer getCost() { // 처음 가격은 4000 원. Integer cost = 4000; if (getHasMilkYn()) { // 우유 가격 추가. cost+=900; } return cost; } } /** * 하우스 블렌딩 방식으로 만든 에스프레소 * * Created by Doohyun on 2017. 6. 25.. */ public class HouseBlendingEspresso extends Coffee{ @Override public Boolean getHasMilkYn() { return false; } /** * 하우스 블렌딩 방식은 일반 에스프레소보다 가격이 1000 원 비쌈. * * @return */ @Override public Integer getCost() { return 1000 + super.getCost(); } } /** * 라떼는 하우스 블렌딩 방식 에스프레소를 사용. * * Created by Doohyun on 2017. 6. 25.. */ public class Latte extends HouseBlendingEspresso{ @Override public Boolean getHasMilkYn() { return true; } } | cs |
이제, 재료에 따라 가격도 연동될 것입니다.
우유 가격을 정확히 명시하였으며, 우유를 사용하는 Latte 는 사용여부만 적절히 구현해주면 됩니다.
(우유 가격 변동에 따라 유연하게 Latte 의 가격을 변동시킬 수 있습니다.)
심지어, 상속관계를 이용하여 Blending 방식에 따른 커피가격 역시 변동할 수 있도록 구현하였습니다.
즉 상속구조 및 적절한 메소드 추가를 통해 부모의 기능에 자신의 입맛에 맞도록 적절한 치장을 하고 있습니다만, 이 구조는 다음과 같은 아쉬움이 존재합니다.
- 재료 추가에따라 Coffee 에 기능을 추가하고, Coffee::getCost 를 수정해야합니다.
- Syrup 을 추가해야한다면, Coffee::getHasSyrupYn 를 추가해야합니다.
- Coffee::getHasSyrupYn 의 추가는, 모든 구현클래스들에 이 기능을 구현해야함을 의미합니다.
(Interface 서명 변경과 같은 문제입니다.)
- 또한, Coffee::getCost 역시 수정해야 합니다.
- 각 최종 구현클래스들은 재료에 유연할 수 없습니다.
- 상속구조 기반으로 재료관계를 명시하였기 때문에, 컴파일 시간에 모든 행위가 결정 됩니다.
- Latte 는 무조건 HouseBlendingEspresso 만 사용합니다.
즉, 다른 블렌딩을 사용하려면 새 클래스를 만들어야 합니다.
- 또한 더블샷, 시럽추가 등의 재료에 대한 유연함을 줄 수 없음을 의미합니다.
즉, 지금 구조는 새로운 요구사항 추가에 있어서 기존 코드를 수정해야할 가능성이 농후하네요..
4. 상속관계에서 연관관계로 리팩토링.
사실 모든 상속관계는 연관관계(has-a) 로 풀 수 있습니다. (Favor has-a over is-a )
우리는 이 규칙을 Observer 패턴의 리뷰 중 [자바 내장 모듈의 한계 및 극복] 에서 다뤄본 적이 있습니다.
연관관계를 이용하면, 상속관계를 실행시간에 동적으로 정의해줄 수 있습니다.
Decorator 패턴은 이러한 연관관계를 이용하여 기능의 변화를 수행하기 위한 패턴입니다.
이를 이용하여 현재 문제로 드러난 "상속에 의한 컴파일 시점에 정의된 재료의 변경 문제" 를 해결할 수 있을 것 같아 보이며, 연관관계이기 때문에 "재료 추가 및 변경에 대해 자유" 를 기대해 볼 수 있을 것 같습니다.
한 번, 장식을 위한 클래스를 다음과 같이 정의해보죠..
장식(Decorator) 객체는 기본적으로 장식 대상 객체와 동일한 Type 이며,
장식된 객체는 기존의 객체를 대신하여 역할을 수행할 수 있도록 구현할 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | /** * 조미료가 되는 클래스 정의. * * Created by Doohyun on 2017. 6. 25.. */ public abstract class CondimentDecorator extends Coffee { private Coffee coffee; /** * 커피를 has-a 관계로 표현. * * @param coffee */ public CondimentDecorator(Coffee coffee) { this.coffee = coffee; } /** * 커피를 출력하도록 변경. * * @return */ protected Coffee getCoffee() { return this.coffee; } } | cs |
생성자 부분에서는 장식대상이 되는 객체를 입력받아 has-a 관계로 유지하고자 합니다.
행위의 추가는 장식대상이 되는 객체를 사용하여, 구현을 할 것입니다.
이제, 아래와 같이 장식을 구현해 볼 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | /** * 우유 데코레이터 * * Created by Doohyun on 2017. 6. 25.. */ public class MilkDecorator extends CondimentDecorator{ public MilkDecorator(Coffee coffee) { super(coffee); } /** * 장식대상 객체이름을 그대로 사용. * * @return */ @Override public String getName() { return getCoffee().getName(); } /** * 우유 가격인 900 원 추가. * * @return */ @Override public Integer getCost() { return getCoffee().getCost() + 900; } } /** * 라떼 데코레이터 * * Created by Doohyun on 2017. 6. 25.. */ public class LatteDecorator extends CondimentDecorator { public LatteDecorator(Coffee coffee) { super(coffee); } /** * 이름을 재정의. * * @return */ @Override public String getName() { return "라떼"; } /** * 가격은 장식 대상 객체의 기존 가격을 그대로 사용. * * @return */ @Override public Integer getCost() { return getCoffee().getCost(); } } | cs |
우유와 관련된 기능은 MilkDecorator 로 구현하여, 장식대상 객체의 가격을 900원 상승시키도록 하였습니다.
LatteDecorator 는 장식대상 객체의 명을 "라떼"로 변경하는 기능을 담당합니다.
가격은 그대로군요.
우리는 이 장식들을 이용하여, HouseBlendingEspresso 객체 대하여 동적으로 상속받는 것과 같은 효과를 줄 수 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | Coffee coffee = new HouseBlendingEspresso(); System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost())); // 우유 데코레이터로 치장. coffee = new MilkDecorator(coffee); // 라떼 데코레이터로 치장. coffee = new LatteDecorator(coffee); System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost())); // CONSOLE LOG // 커피명 : 하우스블렌딩 에스프레소, 커피가격 : 5000 // 커피명 : 라떼, 커피가격 : 5900 | cs |
만약 샷을 추가하는 기능을 제작한다면,
마찬가지로 ShotDecorator 를 제작하여 치장을 해줄 수 있을 것 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * 샷을 추가하는 데코레이터 * * Created by Doohyun on 2017. 6. 25.. */ public class ShotDecorator extends CondimentDecorator{ public ShotDecorator(Coffee coffee) { super(coffee); } @Override public Integer getCost() { return getCoffee().getCost() + 500; } @Override public String getName() { return getCoffee().getName(); } } Coffee coffee = new HouseBlendingEspresso(); System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost())); // 우유 데코레이터로 치장. coffee = new MilkDecorator(coffee); // 라떼 데코레이터로 치장. coffee = new LatteDecorator(coffee); // 더블샷 데코레이터 치장. coffee = new ShotDecorator(coffee); coffee = new ShotDecorator(coffee); System.out.println(String.format("커피명 : %s, 커피가격 : %d", coffee.getName(), coffee.getCost())); // CONSOLE LOG // 커피명 : 하우스블렌딩 에스프레소, 커피가격 : 5000 // 커피명 : 라떼, 커피가격 : 6900 | cs |
이러한 장식구조를 이용하여 동적으로 상속구조를 취할 수 있으며,
자주 사용할 법한 클래스(이를테면, Americano, Latte)들은 내부에서 일정한 방식으로 장식하도록 하는 프리셋을 만들어 제공해 줄 수 있을 것 같군요.
[샷추가나 시럽추가]와 같은 추가기능에도 대응이 가능할 것 같습니다.
Decorator 패턴은 이와 같이 행위 추가를 하고자 하는 타겟 객체를 has-a 관계로 유지함으로써, 적절하게 기존 기능에 행위를 추가를 할 수 있는 패턴입니다.
이는 상속을 이용하여 메소드를 재정의하는 것과 비슷하지만,
- 실행시간에 동적으로 상속구조를 흉내낼 수 있다는 점
- 상속구조에 따라 클래스를 생성하는 것이 아닌, 장식의 조립을 통해 객체를 생성한다는 점
은 복잡한 클래스의 상속구조에서 벗어나는 것에 큰 도움이 될 것입니다.
위의 예제에 대하여, 정리가 된 [실행 가능 프로젝트] 를 제공하며 이번 포스팅을 마무리합니다.
이 글이 모두에게 도움이 되길 바랍니다.
'스터디 > [STUDY] Effective OOP' 카테고리의 다른 글
[CHAPTER 16] Iterator Pattern - Review (0) | 2017.08.02 |
---|---|
[CHAPTER 13] Builder Pattern - Review (0) | 2017.07.20 |
[CHAPTER 11] State Pattern - review (1) | 2017.07.12 |
[CHAPTER 10] Command Pattern - review (0) | 2017.07.06 |
[CHAPTER 7] Facade Pattern - review (0) | 2017.06.29 |