정리정리정리

디자인패턴 - 데코레이터 패턴 (decorator pattern) 본문

JAVA/DesignPattern

디자인패턴 - 데코레이터 패턴 (decorator pattern)

_JSPark 2016. 5. 4. 21:55

데코레이터 패턴 (decorator pattern)


객체에 추가적인 요건을 동적으로 첨가한다.

데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.



데코레이터 패턴 클래스 다이어그램




ConcreteComponent에 새로운 행동을 동적으로 추가할수 있다.

각 데코레이터 안에는 구성요소(Component)에 대란 레퍼런스가 들어있는 인스턴스 변수가있다.

Decorator는 자신이 장식할 구성요소(Component)와 같은 인터페이스 또는 추상 클래스를 구현한다.

ConcreteDecoratorA, ConcreteDecoratorB 에는 그 객체가 장식하고있는(데코레이터가 감싸고 있는 Component객체)을 위한 인스턴스 변수가 있다. 따라서 데코레이터는 Component의 상태를 확장할 수 있다.

ConcreteDecoratorA, ConcreteDecoratorB 데코레이터에서 새로운 메소드를 추가할 수도 있다. 하지만 일반적으로 새로운 메소드를 추가하는 대신 Component에 원래 있던 메소드를 호출하기 전, 또는 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.






문제의 시작


커피가게 사업을 시작하여 주문 시스템을 도입했다고 가정할때.


초기의 모습.



커피를 주문할 때는 스팀 우유나 두유, 모카를 추가하고, 그 위에 휘핑 크림을 얹기도 한다.


각각을 추가할 때마다 커피 가격이 올라가기 때문에 주문 시스템에서도 그런 점들을 모두 고려해야 한다.




한눈에 봐도 뭔가 변화가 필요하군...



그렇다면 슈퍼클래스에 우유, 두유, 모카 같은 옵션여부를 나타내는 인스턴스 변수를 넣으면 어떨까?



서브클래스들에서 각 가격을 계산하고 슈퍼클래스에서 구현한 cost()를 호출하여 추가가격을 구한다면

아마 이런 식이 되겠지..



 public class Beverage{

// 변수 메소스

public int cost(){

int totCost = 0;

if(hasMilk()) totCost += milkCost;

if(hasSoy()) totCost += soyCost;

if(hasMocah()) totCost += mocahCost;

if(hasWhip()) totCost += whipCost;

return totCost;

 }


 public class DarkRoast extends Beverage{


@Override

public int cost() {

return 4500+super.cost();

}


 }



첨가물의 가격이 바뀔 때마다 기존 코드 수정이 필요하고.

첨가물의 종류가 많아지면 새로운 메소드를 추가해야하고, 수퍼클래스의 cost()메소드도 계속 고쳐줘야한다.

새로운 음료가 나왔을때 특정 첨가물이 들어가면 안되는 경우가 있을 것.. 하지만 여전히 슈퍼클래스에서 모두 상속받음.

만약 더블 모카를 계산해야 한다면.


당장은 해결된것 같은데.. 나중에 디자인이 어떻게 바뀌어야 할지 생각해보면 이 접근법에도 문제가 있는것 같다.



디자인 원칙

OCP(Open-Closed principle)
클래스는 확장에 대해서는 열려 있어야하지만 코드 변경에 대해서는 닫혀 있어야 한다.



기존의 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할수 있는 데코레이터 패턴을 사용해보자.


상속을 써서 음료 가격과 첨가물 가격을 합한 총 가격을 계산하는 방법은 그리 좋은 방법이 되지 못했다.

클래스가 어마어마하게 많아지거나 일부 서브 클래스에는 적합하지 않은 기능을 클래스에 추가하게 되는 문제가 있었다.


우선 특정 음료에서 시작해서 첨가물로 그 음료를 장식 해보자.


1. DarkRoast 객체를 가져온다.

2. Mocha 객체로 장식한다.

3. Whip 객체로 장식한다.

4. cost() 메소드를 호출한다. 이때 첨가물의 가격을 계산하는 일은 해당 객체들에게 위임된다.



새로 디자인한 Beverage 클래스





디자인을 바탕으로 코드를 만들어보자.


public abstract class Beverage{

private String description = "Empty";

public String getDescription(){

return this.description;

}

public abstract int cost();

 }



 public abstract class CondimentDecorator extends Beverage{

public abstract String getDescription();

 }



 public class Espresso extends Beverage{

public Espresso() {

this.description = "에스프레소";

}


@Override

public int cost() {

return 3500;

}


 }



 public class HouseBlend extends Beverage{

public HouseBlend() {

this.description = "하우스블렌드";

}


@Override

public int cost() {

return 2000;

}


 }



public class Mocha extends CondimentDecorator{

private Beverage beverage;

public Mocha(Beverage beverage) {

this.beverage = beverage;

}


@Override

public String getDescription() {

return beverage.getDescription()+", 모카";

}


@Override

public int cost() {

return 500+beverage.cost();

}

 }



 public class CoffeeStore{

public static void main(String args[]){

Beverage beverage = new Espresso();

System.out.println(beverage.getDescription()+" cost : "+beverage.cost());

Beverage beverage1 = new DarkRoast();

beverage1 = new Mocha(beverage1);

beverage1 = new Mocha(beverage1);

beverage1 = new Whip(beverage1);

System.out.println(beverage1.getDescription()+" cost : "+beverage1.cost());

Beverage beverage2 = new HouseBlend();

beverage2 = new Soy(beverage2);

beverage2 = new Mocha(beverage2);

beverage2 = new Whip(beverage2);

System.out.println(beverage2.getDescription()+" cost : "+beverage2.cost());

}

 } 



데코레이터가 적용된 예 : 자바 I/O


개발하면서 스트림의 개념이 잘 잡히지 않았을때.. 자바 I/O API를 보고 한숨을 쉬는 사람들이 나말고도 많았을 거라 생각한다.

기반스트림과 보조스트림을 데코레이터 패턴을 배우고 나서 머리속에서 다시 정리해보면 많은 클래스들이 좀더 친근하게 다가 온다.


실제 자바에서 클래스 다이어그램을 그려보면.




InputStream 이 추상구성요소이고 모든 보조스트림의 조상인 FilterInputStream 이 추상 데코레이터 이다.

FilterInputStream을 상송받아 구현하는 BufferedInputStream 클래스들이 구상 데코레이터이다.

InputStream을 상속받는 FileInputStream 같은 기반 스트림들은 데코레이터로 포장될 구상 구성요소 역할을 한다.














참고.


Head First Design Pattern.

3 Comments
  • 프로필사진 김개똥 2018.08.26 11:20 위의 데코레이션 패턴 설명 잘 배웠습니다.
    설명중에서..
    "디자인을 바탕으로 코드를 만들어보자.
    public abstract class Beverage {
    ......
    ......
    public abstract void cost();
    } "

    여기서 cost()함수는 커피나 커피첨가물의 가격을 계산하는 역할을 하는데 리턴값이 void로 되어있습니다. 계산한 결과값을 받을 수 있어야 할거 같은데요.

    그리고 두 번째 질문으로는

    Beverage beverage2 = new HouseBlend();
    beverage2 = new Soy(beverage2);
    beverage2 = new Mocha(beverage2);
    beverage2 = new Whip(beverage2);
    System.out.println(beverage.getDescription() + " cost; " + beverage2.cost());
    "
    여기에서 보면 HouseBlend라는 커피에 첨가물로 Soy, Mocha, Whip이 차례대로 추가되는 과정으로 이해됩니다. 추가를 마친 후 beverage2.getDescription()으로 불러왔을 때 어떤 내용이 나오게 되나요?

    HouseBlend ?
    HouseBlend, Soy ?
    HouseBlend, Soy, Mocha ?
    HouseBlend, Soy, Mocha, Whip ?
    Housebound, Whip ?
  • 프로필사진 _JSPark 2018.09.05 00:10 신고 안녕하세요.
    네 HouseBlend, Soy, Mocha, Whip 입니다.
  • 프로필사진 redbean88 2020.07.10 11:07 안녕하세요 포스팅 잘 봤습니다.
    실습하면서 궁금한 점이 생겨서 몇가지 문의 드립니다.
    1. Beverage 클래스에 전역변수의 필요성 여부
    - get메소드를 이용하여 원하는 인스턴스만 반환해주면 되는거 아닌가요?
    2. CondimentDecorator 의 추상클래스 필요성
    - 해당 메소드를 구현하지 않아도 정상 작동되는데, 구현체에게 해당 구현정보를 알려주기 위한 용도인건가요?

    감사합니다.
댓글쓰기 폼