데코레이터 패턴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() 메소드를 추상메소드로 만들지 않고 추가 토핑 비용을 합산한 가격을 리턴하도록 만들어 보죠.
xxxxxxxxxx
public 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;
}
}
xxxxxxxxxx
public 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 = "최고의 다크 로스트 커피";
}
}
xxxxxxxxxx
public 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 코드는 처음에 만들었던 클래스를 그대로 씁니다.
xxxxxxxxxx
public abstract class Beverage {
protected String description = "제목 없음";
public String getDescription() {
return description;
}
abstract public int cost();
}
첨가물 클래스 (데코레이터 클래스)
- 각 첨가물에 대해 getDescription() 메소드를 구현하도록 만들 계획이라 추상클래스로 재선언
xxxxxxxxxx
public abstract class CondimentDecorator extends Beverage{
public abstract String getDescription();
}
2.2.2 음료 클래스
- Beverage클래스로 부터 상속받은 description 멤버변수 값 설정
- 음료 클래스의 cost() 메소드는 가격만 return 하도록 구현
xxxxxxxxxx
public class Espresso extends Beverage {
public Espresso() {
this.description = "에스프레소";
}
public int cost() {
return 3500;
}
}
2.2.3 첨가물 클래스
- 첨가물 클래스에는 감싸고자 하는 Beverage 인스턴스 변수와 그 객체를 설정하기 위한 생성자가 필요
- 음료에 어떤 첨가물을 추가했는지 설명하기 위해 getDescription() 메소드 구현
- 음료가격에 첨가물을 추가한 가격을 리턴하도록 cost() 메소드 구현
xxxxxxxxxx
public 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();
}
}
xxxxxxxxxx
public 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();
}
}
xxxxxxxxxx
public 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();
}
}
xxxxxxxxxx
public 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 주문 코드
xxxxxxxxxx
public 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 |
댓글