티스토리 뷰
데코레이터 패턴1. 스타벅스초창기 스타벅스스타벅스 시스템 개선여전히 문제는 존재한다2. 데코레이터 패턴2.1 스타벅스 클래스 구성도2.2 소스코드2.2.1 추상 클래스2.2.2 음료 클래스2.2.3 첨가물 클래스2.2.4 주문 코드2.3 데코레이터 패턴 단점3. 자바 I/O와 데코레이터 패턴
데코레이터 패턴
- 비교적 배우기 쉬운 패턴
- 상속을 남용하는 전형적인 예를 살펴보고, 실행중에 클래스를 꾸미는(데코레이션) 방법을 배워봅시다.
- 데코레이터 패턴은 말 그대로 객체를 이렇게 저렇게 꾸미는 패턴입니다
1. 스타벅스
- 스타벅스의 커피는 메뉴도 다양하고 고객의 기호에 따라 메뉴를 커스터마이징(두유 변경, 샷 추가, 휘핑 추가, 스팀밀크 추가 등)을 할 수 있습니다.
- 이런 스타벅스 커피 시스템을 코드로 구현해봅시다.
초창기 스타벅스
초창기에는 메뉴가 다양하지 않았습니다.
그래서 음료의 공통사항을 묶은 Beverage 추상 클래스를 만들어서 해당 클래스를 상속 받도록 구현했죠
- 각 음료의 설명을 담을 변수 description과 음료 가격을 리턴하는 cost() 메소드가 있습니다.

처음에는 클래스가 5개 였습니다.
그러나 스타벅스가 점점 발전하면서 스팀우유, 두유, 모카, 휘핑 크림 등을 추가할 수 있게 되었습니다.
그래서 조합할 수 있는 모든 종류의 음료 클래스들을 만들었죠. 처음엔 이랬습니다.
Decaf
DecafWithWhip
- DecafWithWhipAndSoy
- DecafWithWhipAndMocha
- DecafWithWhipAndSteamedMilk
DecafWithSteamedMilk
- DeafWithSteamedMilkAndMocha
- DeafWithSteamedMilkAndSoy
DecafWithMocha
- DecafWithMochaAndSoy
DecafWithSoy
DarkRoast
- 기타 등등..
Espresso
- 기타 등등..
HouseBlend
기타 등등..
만약 우유의 가격이 오르거나 새로운 토핑을 추가하면 어떻게 될까요?
- 우유가 포함된 모든 클래스를 수정해야 하고
- 새로운 토핑에 대한 수 많은 조합의 클래스를 생성해야합니다.
스타벅스 시스템 개선
늦기 전에 시스템을 얼른 수정해야합니다.
- 토핑에 대한 내용을 클래스로 만들지 않고, 인스턴스 변수로 만들어서 관리해 봅시다.
- 그리고 cost() 메소드를 추상메소드로 만들지 않고 추가 토핑 비용을 합산한 가격을 리턴하도록 만들어 보죠.
xxxxxxxxxxpublic abstract class Beverage { protected String description; private boolean milk; private boolean soy; private boolean mocha; private boolean whip; private int milkCost; private int soyCost; private int mochaCost; private int whipCost; public Beverage2() { this.milkCost = 500; this.soyCost = 700; this.mochaCost = 1000; this.whipCost = 300; } public void setMilkCost(int milkCost) { this.milkCost = milkCost; } public void setSoyCost(int soyCost) { this.soyCost = soyCost; } public void setMochaCost(int mochaCost) { this.mochaCost = mochaCost; } public void setWhipCost(int whipCost) { this.whipCost = whipCost; } public boolean hasMilk() { return milk; } public void setMilk(boolean milk) { this.milk = milk; } public boolean hasSoy() { return soy; } public void setSoy(boolean soy) { this.soy = soy; } public boolean hasMocha() { return mocha; } public void setMocha(boolean mocha) { this.mocha = mocha; } public boolean hasWhip() { return whip; } public void setWhip(boolean whip) { this.whip = whip; } public void setDescription(String description) { this.description = description; } public String getDescription() { return description; } public int cost() { int totalCost = 0; if (hasMilk()) { totalCost += milkCost; } if (hasMocha()) { totalCost += mochaCost; } if (hasSoy()) { totalCost += soyCost; } if (hasWhip()) { totalCost += whipCost; } return totalCost; }}xxxxxxxxxxpublic class DarkRoast extends Beverage { private int cost; DarkRoast(int cost) { setDescription(); this.cost = cost; } public DarkRoast() { setDescription(); this.cost = 4500; } public void setCost(int cost) { this.cost = cost; } public int cost() { return this.cost + super.cost(); } private void setDescription() { description = "최고의 다크 로스트 커피"; }}xxxxxxxxxxpublic class Client { public static void main(String[] args) { DarkRoast darkRoast = new DarkRoast(); darkRoast.setMilk(true); darkRoast.setMocha(true); System.out.println("커피가격:" + darkRoast.cost()); System.out.println("<< 원자재 가격 상승 >>"); DarkRoast darkRoastPriceRise= new DarkRoast(5100); darkRoastPriceRise.setMilkCost(1500); darkRoastPriceRise.setMochaCost(2000); darkRoastPriceRise.setMilk(true); darkRoastPriceRise.setMocha(true); System.out.println("커피가격:" + darkRoastPriceRise.cost()); }}
- 음료 클래스 1개와 하위 메뉴 클래스 4개 총 5개의 클래스로, 가격도 쉽게 변경 가능하고 재료도 쉽게 넣거나 뺄 수 있게 되었습니다.
- 이러한 방식에는 문제가 없을까요?
여전히 문제는 존재한다
우유, 모카와 같은 첨가물의 종류가 증가할때마다 새로운 멤버 인스턴스와 메소드를 추가해야하고, 슈퍼클래스의 cost() 메소드를 수정해야합니다.
새로운 음료가 추가되었는데, 그 중에는 특정 첨가물이 들어가면 안 되는 경우도 있습니다.
- 아이스티로 예를 들면 Tea 서브클래스에서 슈퍼클래스 메소드 hasWhip()같은 메소드를 여전히 상속 받게 될 것입니다.
더블 샷 추가와 같이 첨가물을 2번 넣고 싶으면 어떻게 해야할까요?
2. 데코레이터 패턴
기존 코드는 건드리지 않고, 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하는게 우리의 목표입니다.
객체를 감싸는 방법을 통해서 위와 같은 목표를 달성할 수 있습니다.
우선 데코레이터 패턴의 정의는 다음과 같습니다.
데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가
데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공

- Decorator는 자신이 장식(데코레이트)할 구성요소와 같은 인터페이스 또는 추상클래스를 구현
- Decorator의 구상클래스인 ConcereteDecorator 클래스에는 구성요소(Component)에 대한 레퍼런스가 들어있는 인스턴스 변수가 있음
2.1 스타벅스 클래스 구성도
- 위와 같은 방식으로 같은 개념을 스타벅스 시스템에 적용해봅시다.

이런식으로 구성을 하면 레퍼런스 인스턴스 변수를 통해 cost() 메소드 호출 시, 차례대로 첨가물과 음료의 가격을 계산하여 최종 가격을 반환 받도록 구현할 수 있음
예를 들어 모카와 휘핑 크림이 추가된 다크 로스트 커피를 주문했다면 아래와 같이 데코레이트 될 것이다.
- DarkRoast 객체를 가져온다.
- Mocha 객체로 장식한다.
- Whip 객체로 장식한다.
- cost() 메소드를 호출한다. (첨가물의 가격을 계산하는 일은 해당 객체들에게 위임된다.)

- 외부에서 cost() 메소드를 호출하면 Whip 인스턴스가 Mocha 인스턴스의 cost()를 호출하고, Mocha 인스턴스는 DarkRoast의 인스턴스를 호출
- 차례대로 4500, 1000, 300을 반환하여 모두 더하면 4800원이 되도록 cost() 메소드를 구현
2.2 소스코드
- 코드의 양을 줄이기 위해 아래 소스코드에서는 음료 및 첨가물의 가격을 고정합니다.
2.2.1 추상 클래스
- 위 구성에서 Beverage 코드는 처음에 만들었던 클래스를 그대로 씁니다.
xxxxxxxxxxpublic abstract class Beverage { protected String description = "제목 없음"; public String getDescription() { return description; } abstract public int cost(); }첨가물 클래스 (데코레이터 클래스)
- 각 첨가물에 대해 getDescription() 메소드를 구현하도록 만들 계획이라 추상클래스로 재선언
xxxxxxxxxxpublic abstract class CondimentDecorator extends Beverage{ public abstract String getDescription();}2.2.2 음료 클래스
- Beverage클래스로 부터 상속받은 description 멤버변수 값 설정
- 음료 클래스의 cost() 메소드는 가격만 return 하도록 구현
xxxxxxxxxxpublic class Espresso extends Beverage { public Espresso() { this.description = "에스프레소"; } public int cost() { return 3500; }}2.2.3 첨가물 클래스
- 첨가물 클래스에는 감싸고자 하는 Beverage 인스턴스 변수와 그 객체를 설정하기 위한 생성자가 필요
- 음료에 어떤 첨가물을 추가했는지 설명하기 위해 getDescription() 메소드 구현
- 음료가격에 첨가물을 추가한 가격을 리턴하도록 cost() 메소드 구현
xxxxxxxxxxpublic class Mocha extends CondimentDecorator { private Beverage beverage; public Mocha(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + " + 모카"; } public int cost() { return 1000 + beverage.cost(); }}xxxxxxxxxxpublic class Soy extends CondimentDecorator { private Beverage beverage; public Soy(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + " + 두유"; } public int cost() { return 700 + beverage.cost(); }}xxxxxxxxxxpublic class Milk extends CondimentDecorator { private Beverage beverage; public Milk(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + " + 우유"; } public int cost() { return 500 + beverage.cost(); }}xxxxxxxxxxpublic class Whip extends CondimentDecorator { private Beverage beverage; public Whip(Beverage beverage) { this.beverage = beverage; } public String getDescription() { return beverage.getDescription() + " + 휘핑"; } public int cost() { return 300 + beverage.cost(); }}2.2.4 주문 코드
xxxxxxxxxxpublic class client { public static void main(String[] args) { //Beverage espresso = new Espresso(); //Mocha mocha = new Mocha(espresso); //Whip whip = new Whip(mocha); Beverage espresso = new Whip(new Mocha(new Espresso())); System.out.println("음료:"+ espresso.getDescription()); System.out.println("가격:"+ espresso.cost()); }}
- 데코레이터의 생성 방식은 조금 지저분합니다. 나중에 팩토리패턴과 빌더패턴을 사용하여 만들게 되면 훨씬 깔끔해 질 것입니다.
2.3 데코레이터 패턴 단점
필요한 기능들을 추가하다보면 잡다한 클래스가 너무 많아집니다.
데코레이터 패턴 기반으로 작성된 API를 사용하여 개발해야하는 사람 입장에서는 괴롭죠
- 처음 우리가 자바 InputStream API를 접했을때를 떠올려봅시다..
3. 자바 I/O와 데코레이터 패턴

- 우리가 흔히 알고있는 InputStream은 대표적인 데코레이터 패턴이 적용된 API중 하나입니다.
- 데코레이터 패턴을 학습하기 전에 저 그림을 보면 단순히 상속관계를 나타낸 그림이라 생각할 것 입니다.
- 정리하자면, InputStream 클래스는 추상클래스이며 FilterInputStream을 제외한 하위 클래스는 이를 구현한 구상 클래스입니다.
- FilterInputStream 클래스는 데코레이터 클래스(추상클래스)입니다.
- 이를 상속 받아 구현한 Buffered, Data, LineNumber, Pushbank 클래스는 InputStream 구상클래스의 데코를 위한 클래스입니다.
'Design pattern' 카테고리의 다른 글
| 옵저버 패턴(observer pattern) 정리 (0) | 2020.06.17 |
|---|---|
| 스트래티지 패턴(strategy pattern) 정리 (1) | 2020.05.20 |
- Total
- Today
- Yesterday
- 구글 드라이브 개발
- SSD
- 후지필름 일렉트로닉
- 링크드리스트 클래스
- C
- 링크드리스트
- 구글 드라이브 API
- ssd추천
- ssd성능
- 후지필름X100V
- X100v
- ssd비교
- 리스트 소스 코드
- 샌디스크ssd
- 후지필름
- SDK
- 삼성ssd
- 구글 드라이브
- 리스트
- 리스트 클래스
- C++
- Google Drive SDK
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
