헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.
데코레이터 패턴을 이용하면 객체에 추가 요소를 동적으로 더할 수 있습니다. 데코레이터를 사용하면 서브 클래스를 만드는 경우에 비해 훨씬 유연하게 기능을 확장할 수 있습니다.
OO커피는 단기간에 급속도로 성장한 대형 커피 전문점이다. 빠르게 성장한 만큼, 음료들을 모두 포괄하는 주문 시스템이 이제서야 개발되려고 하는 상황이다. 처음 시스템 시작할 무렵에 만들어진 클래스는 위의 사진과 같다.
Beverage : 음료를 나타내는 추상 클래스, 모든 음료는 해당 클래스의 서브클래스
이처럼 클래스 개수가 폭발적으로 증가하게 된다. 만약 우유나 크림 가격이 인상된다면?? 한눈에 보기에도 이렇게 수많은 클래스를 관리하기는 힘들 것이다.
public class Beverage {
String description;
boolean hasMilk, hasSoy, hasMocha;
double milkCost, soyCost, mochaCost;
public double cost() {
double condimentCost = 0;
if (getHasMilk()) {
condimentCost += milkCost;
}
if (getHasSoy()) {
condimentCost += soyCost;
}
if (getHasMocha()) {
condimentCost += mochaCost;
}
return condimentCost;
}
// get, set..
public boolean getHasMilk() {
return hasMilk;
}
public boolean getHasSoy() {
return hasSoy;
}
public boolean getHasMocha() {
return hasMocha;
}
}
public class DarkRoast extends Beverage{
public DarkRoast() {
description = "다크 로스트";
}
@Override
public double cost() {
return super.cost() + 3500;
}
}
상속은 객체지향 디자인의 강력한 요소 중 하나지만, 이처럼 상속을 사용한다고 해서 무조건 유연하고 관리하기 쉬운 디자인이 만들어지지 않는다. 그 이유는 서브 클래스를 만드는 방식으로 행동을 상속 받으면 해당 행동은 컴파일시에 완전히 결정되고 모든 서브클래스에서 슈퍼 클래스의 멤버들을 상속 받아야 하기 때문이다. 하지만 composite를 통해서 객체의 행동을 실행 중에 동적으로 설정하는 방법을 사용한다면, 즉 객체를 동적으로 구성하면, 기존 코드를 수정하는 대신 새로운 코드를 추가하는 방식으로 새로운 기능을 추가할 수 있다. 기존 코드는 수정되지 않으므로(변경에 대해서는 닫혀있으므로) 버그가 생기거나 사이드 이펙트를 방지하면서 새로운 기능을 추가(확장에 대해서는 열려있는)할 수 있는 것이다.
이제 음료에서 추가되는 옵션이 있는 경우 해당 음료를 데코레이터 하는 방식으로 수정해보자. 만약 모카와 휘핑 크림을 추가한 다크 로스트 커피는 다음처럼 할 수 있을 것이다.
이 때 장식하고 위임하는 방법은 해당 객체를 래퍼 객체라고 생각하면 쉽다.
이렇게 가장 바깥쪽에 있는 데코레이터 객체에서 cost()를 호출하고, 해당 객체가 장식하고 있는 객체에게 가격을 위임한다. 위임한 객체에게 가격의 값을 얻으면, 자신의 가격을 더한 다음 리턴하는 것이다.
여기서 중요한 점은 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수도 있다는 저이다.
데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가한다. 데코레이터는 서브클래스를 만드는 것을 통해서 기느을 유연하게 확장할 수 있는 방법을 제공한다.
Compent
ConcreteComponent
Decorator
ConcreteDecorator
public abstract class Beverage {
String description = "";
public abstract double cost();
public String getDescription() {
return description;
}
}
public class Espresso extends Beverage {
public Espresso() {
description = "에스프레소";
}
@Override
public double cost() {
return 3500;
}
}
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
public class Mocha extends CondimentDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
@Override
public double cost() {
return 500 + beverage.cost();
}
@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
}
}
public class StarbuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + ": " + beverage.cost() + " won");
Beverage beverage2 = new Espresso();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
System.out.println(beverage2.getDescription() + ": " + beverage2.cost() + " won");
}
}
데코레이터 패턴을 적용한 코드는 아까의 문제점이 사라진 오류의 코드지만 저런형태로 관리하게 될경우 마지막 Soy를 빼먹는다던가 실수로 두번넣는 경우가 생기게됩니다. 팩토리 패턴과 빌더 패턴을 이용해서 더 쉽게 객체를 만드는 방법이 존재한다.