'Decorator Pattern'에 해당되는 글 2건

  1. 2012.10.29 Decorator 패턴에 대한 생각 정리 (1)
  2. 2012.06.04 Decorator Pattern (데코레이터 패턴)

안녕하세요.

오늘은 Decorator 패턴에 대해 생각이 정리된 부분을 이야기 해볼까 합니다.


Decorator 패턴에 대한 설명은 위의 링크를 따라 들어가서 보시면 될 것 같구요.


Decorator패턴은 굉장히 간단하고 멋져보이는 패턴입니다.

저도 처음 봤을 때 "우와~ 이런방법이 있구나~" 하면서 놀랐죠.


하지만 막상 실전에 어떻게 활용해야할 지 막막하더군요...

그래서 내린 결론은 

디자인 패턴을 이해한다는 것은 단순히 클래스 관계를 이해하는 것뿐만 아니라, 언제 어떤 의도로 이 패턴이 사용될 수 있는지를 이해하는게 더 중요하다.

라는 것 입니다.


많은 책에서 Decorator패턴을 설명할 때에 커피의 가격 계산에 대한 예를 사용하거나, 윈도우에 스크롤뷰와 프레임을 붙이는 등의 상황을 예로 듭니다. 


그런데 아무리 생각해봐도 이러한 상황에 Decorator패턴을 쓰는게 가능한지 잘 모르겠더군요.




먼저, 커피의 경우에는 Decorator패턴이 정말이지 가격 계산에만 쓰일 수 있는 것 같습니다. 즉, 커피 자체에 대해 Decorator패턴을 적용할 수 있는게 아니라, 커피의 가격에 대해서만 적용이 가능하다는 것입니다. (물론, 커피의 가격만 계산하는 프로그램이라면 전혀 문제될 것이 없지만, 커피의 향이나 색 등을 표현하기에는 Decorator 패턴만으로는 부족하다는 것입니다.)


윈도우 예의 경우에는 윈도우에 스크롤을 입힌다는데, 스크롤 뷰라는게 단순히 뷰만 있는게 아니고 윈도우 내의 컨텐츠 움직임에 따라 스크롤의 길이와 위치가 변화하게 되는데 이런 부분까지 다 표현하는게 가능할까...라는 점입니다.

(이를 Decorator패턴으로 설계 가능하다고 생각하시는 분은 저에게 가르침을 주세요~^^)




이렇게 기존의 예에 대한 이해부족으로 실제로 적용 가능한 상황이 어떤게 있을까.. 하고 생각을 해보게 되었습니다.


터치 효과 Touch Effect

제가 생각한 예는 화면을 터치했을 때 특수한 효과가 나는 예입니다. 

화면을 터치하면 진동이 나올 수도 있고, 터치한 주위로 꽃들이 터질 수도 있고, 화면이 깨지는 효과가 나올 수도 있죠.

또 이들 효과가 2개 이상 동시에 발동될 수도 있습니다.


이러한 효과가 한번에 하나만 가능하다면 Strategy 패턴같은 방법으로 구현이 가능하겠죠. 

하지만 동시에 여러가지 효과가 발동되어야 한다면 Strategy패턴으로 구현하기보다는 Decorator패턴을 이용하는게 좋을 것 같습니다. 


먼저 클래스 다이어그램부터 보시죠.




클래스 구조는 전형적인 Decorator패턴의 모습입니다.

이렇게 Decorator패턴을 이용해서 설계를 하면 생길 수 있는 장점에 대해 이야기를 해보죠.


1. 상태 변수를 이용해 프로그래밍하는 것보다 훨씬 코드가 간결해진다.

첫 번째 장점은 상태 변수를 이용해 프로그래밍하는 것보다 코드가 간결해진다는 점입니다.

즉, 터치 효과를 저장하는 int형의 멤버 변수를 만들고 이 멤버 변수에 어떤 효과들이 설정되어있는지 설정값을 담아둘 수 있습니다. 


public static final int TOUCH_EFFECT_FLOWER = 1 << 0;
public static final int TOUCH_EFFECT_CRACK = 1 << 1;
public static final int TOUCH_EFFECT_VIBRATION = 1 << 2;

private int _touchEffects = TOUCH_EFFECT_FLOWER | TOUCH_EFFECT_CRACK;


이렇게 _touchEffects 변수에 터치 효과들을 담아두고 있다가, 사용하는 곳에서 변수를 체크해서 적절한 효과를 발동시키는 겁니다. 

 

public void onTouch(float x, float y) {
    if(_touchEffects & TOUCH_EFFECT_FLOWER == TOUCH_EFFECT_FLOWER) {
        //꽃 효과를 발동한다.
    }

    if(_touchEffects & TOUCH_EFFECT_CRACK == TOUCH_EFFECT_CRACK) {
        //액정이 깨진 효과를 발동한다.
    }

    if(_touchEffects & TOUCH_EFFECT_VIBRATION == TOUCH_EFFECT_VIBRATION) {
        //진동 효과를 발동한다. 
    }
}


이렇게 사용하는 터치 효과를 발동해야 하는 곳에서 _touchEffects 변수를 체크해서 사용해야 합니다.

이렇게 사용하는 것의 단점이 뭘까요?

솔직히 복잡하게 설계를 하는 것보다 이렇게 쓰는게 훨씬 빨리 코딩할 수 있는 방법아닐까요?


지금 보여진 코드만 보면 그렇습니다.

하지만,, _touchEffects라는 변수는 여기에서만 사용되지 않을 수도 있습니다.

다른 메서드에서도 저런 분기문을 똑같이 사용할 수 있습니다.

또 효과가 하나씩 늘어날 때마다 _touchEffects 변수를 사용하는 곳의 코드를 또 추가적으로 작성해야 하구요.

그렇게 _touchEffects를 사용하는 메서드가 늘어날수록 코드는 더욱더 복잡해지고, 수정을 위해 많은 시간과 노력을 요할 수 있습니다.


2. Strategy 패턴을 이용하는 설계보다 클래스 수를 훨씬 줄일 수 있다.

만약 Decorator패턴을 사용하지 않고, Strategy패턴을 이용해 설계를 하면 이 경우에는 훨씬 많은 수의 클래스가 필요할 수 있습니다.

혹시 오해하실까봐 말씀드리는데 Strategy패턴이 Decorator패턴보다 좋지 않다는게 아니라, 지금 제가 말씀드리고 있는 이 상황에서 그렇다는 겁니다.  



먼저 이 상황을 다시 한번 설명 드리자면, 동시에 여러개의 효과가 발동될 수 있습니다.


즉, 꽃 효과와 진동효과가 동시에 발동되거나 진동효과와 액정 깨짐 효과가 동시에 발동될 수도 있습니다.

이를 Strategy패턴으로 구현하려면 TouchEffect인터페이스를 상속받는 FlowerTouchEffect, VibrationTouchEffect, CrackTouchEffect뿐만 아니라 이들을 조합한 FlowerVibrationTouchEffect, FlowerCrackTouchEffect ..등등 많은 수의 서브 클래스가 필요하게 됩니다.


정리

이렇게 수평적인 관계의 여러 기능을 동적으로 결합해야하는 경우에 Decorator패턴이 잘 사용될 수 있을 것 같습니다. 


또 다르게 사용될 수 있는 방법이 생각난다면 다음 번에 포스팅하기로 하죠~^^

저작자 표시 비영리 변경 금지
신고

'Design Patterns > 내 생각' 카테고리의 다른 글

Decorator 패턴에 대한 생각 정리  (1) 2012.10.29
싱글톤과 상태 값  (0) 2012.08.01
Posted by Code-Moon

댓글을 달아 주세요

  1. ㅁㄴㅇㄹ 2012.12.01 22:02 신고 Address Modify/Delete Reply

    잘 보고 갑니다 ^^ Decorator 패턴을 쓰면 switch문을 쓰는 것보다 재사용성하고, 유지보수가

    쉬워지는 장점이 있네요 ㅎㅎ

먼저 이글은 영문 위키의 글을 번역한 글임을 알려드립니다.
영어 실력이 부족한 관계로 오역이 있을 수도 있습니다.


구조적 패턴 - 데코레이터 패턴




객체 지향 프로그래밍에서 데코레이터 패턴은 이미 존재하는 객체에 동적으로 행위(동작, behaviour)를 추가할 수 있도록 하는 디자인 패턴이다.

도입 Introduction

데코레이터 패턴은 런타임에 특정 객체의 기능을 확장하는데 사용될 수 있는데, 이 때 같은 클래스의 다른 객체에는 무관하게 기능을 확장할 수 있고,  이는 기초작업이 설계할 때 이미 완료되었을 때 가능한 일이다. 이는 원래(original) 클래스를 감싸는 새로운 데코레이터 클래스를 설계함으로써 얻을 수 있는 성과이다. 이렇게 데코레이터로 감싸는 것은 아래의 순서에 따라 하게 된다.

  1. 원래의(original) "Decorator"클래스를 "Component"클래스로부터 서브 클래싱한다.(뒤에 나오는 UML 다이어그램을 보라.)
  2. Decorator 클래스에서 Component에 대한 포인터를 필드(멤버 변수)로 추가해라.
  3. Component를 Decorator의 생성자에 인자로 전달해서, Decorator클래스에서 Component 포인터를 초기화할 수 있도록 하라.
  4. Decorator클래스에서 모든 "Component" 메서드를 "Component" 포인터로 재전송하라(redirect. 이 말을 다시 설명하자면, Component 클래스에 있는 operation()이라는 메서드를 Decorator에서 오버라이드를 하게 되는데, 이 때 Decorator에서 포인터로 가지고 있는 Component객체의 operation()을 다시 호출하라는 의미입니다. )
  5. ConcreteDecorator 클래스에서 Component 클래스의 수정이 필요한 메서드를 오버라이드 하라.


이 패턴은  매번 오버라이드된 메서드에 새로운 기능이 추가될 때마다(새로운 데코레이터로 감쌀때마다), 여러개의 데코레이터들이 스택처럼 쌓일 수 있게 설계한다.

데코레이터 패턴은 서브 클래싱의 대안이다. 서브 클래싱은 컴파일할 때 행동을 추가하게 되고, 그때 클래스의 변화는 모든 원래의(original) 클래스의 객체에 대해 변화를 일으킨다. ; 꾸미는 것(decorating)은 런타임에 각각의 객체에 새로운 행위를 제공할 수 있다. 


이러한 차이는 기능을 확장하는 몇몇 독립적인 방법들이 있을 때 아주 중요하게 다가온다. 일부 객체 지향 프로그래밍 언어에서는 클래스가 런타임에 생성될 수 없고, 전형적으로(보통, typically) 이는 설계할 때에 어떤 확장의 조합이 필요한지 예측할 수 없게된다. 이는 새로운 클래스는 모든 가능한 조합에 대해 만들어져야 함을 의미할지도 모른다. 대조적으로 데코레이터는 런타임에 만들어지는 객체라서 사용할 때마다 조합될 수 있다. 자바와 .NET 프레임웍의 I/O Streams 구현은 데코레이터 패턴을 포함하고 있다.

동기 Motivation

한 예로써, 윈도우 시스템의 윈도우를 생각해보자. 윈도우의 컨텐츠를 스크롤할 수 있도록 하기 위해, 우리는 수평 스크롤바나 수직 스크롤바를 추가하고 싶을 수도 있다. 윈도우가 Window클래스의 객체에 의해 표현된다고 가정하고, 이 클래스는 스크롤바를 추가하는 기능이 없다고 가정하자. 우리는 스크롤 기능을 제공하는 ScrollingWindow를 Window로 부터 서브클래싱하여 만들거나, 이미 존재하는 Window객체에 스크롤 기능을 추가하는 ScrollingWindowDecorator를 만들 수 있다. 이 상황에서는 둘 중 어떤 해결책도 괜찮다.


그렇다면 이번에는 우리의 윈도우에 경계선(borders)을 추가하고 싶다고 가정해보자. 마찬가지로 원래의 Window클래스는 이러한 기능을 지원하지 않는다. ScrollingWindow 서브클래스는 효과적으로 새로운 윈도우를 만들었기 때문에 한가지 문제가 생겼다. 

만약 우리가 모든 윈도우에 경계선을 추가하고 싶다면 WindowWithBorder라는 서브클래스와 ScrollingWindowWithBorder라는 서브클래스를 만들면 된다. 확실히 이런 문제는 모든 새로운 기능이 추가될 때마다 더 문제가 된다. 

데코레이터 방법으로는 런타임에 간단히 BorderWindowDecorator를 만들면 된다. 우리는 이미 존재하는 윈도우를 ScrollingWindowDecorator나 BorderedWindowDecorator로 꾸밀 수 있다.


데코레이터가 사용되면 좋은 또다른 상황은 어떤 규칙에 따라 객체의 속성이나 메서드에 대한 접근을 한정지어야 할 필요가 있을 때 이다. (예를들면 다른 사용자 자격인증서.) 이러한 경우에 원래의 객체에 접근 제어를 구현하는 것보다는 원래 객체는 변화시키지 않고 속성이나 메서드 사용에 대해 아무것도 모르도록하고, 접근 제어를 할 수 있는 데코레이터 객체로 감싸면 오직 승인된 인터페이스만 제공하도록 할 수 있다.

구조 Structure


예 Examples

자바

첫번째 예 (window / scrolling 시나리오)

아래의 자바 예는 window/scrolling 시나리오에서 데코레이터의 사용을 설명하고 있다.

// the Window interface
interface Window {
    public void draw(); // draws the Window
    public String getDescription(); // returns a description of the Window
}
 
// implementation of a simple Window without any scrollbars
class SimpleWindow implements Window {
    public void draw() {
        // draw window
    }
 
    public String getDescription() {
        return "simple window";
    }
}


아래의 클래스는 모든 Window클래스(데코레이터 자신들을 포함한)에 대한 데코레이터들이다.

// abstract decorator class - note that it implements Window
abstract class WindowDecorator implements Window {
    protected Window decoratedWindow; // the Window being decorated
 
    public WindowDecorator (Window decoratedWindow) {
        this.decoratedWindow = decoratedWindow;
    }
    public void draw() {
        decoratedWindow.draw();
    }
}
 
// the first concrete decorator which adds vertical scrollbar functionality
class VerticalScrollBarDecorator extends WindowDecorator {
    public VerticalScrollBarDecorator (Window decoratedWindow) {
        super(decoratedWindow);
    }
 
    public void draw() {
        decoratedWindow.draw();
        drawVerticalScrollBar();
    }
 
    private void drawVerticalScrollBar() {
        // draw the vertical scrollbar
    }
 
    public String getDescription() {
        return decoratedWindow.getDescription() + ", including vertical scrollbars";
    }
}
 
// the second concrete decorator which adds horizontal scrollbar functionality
class HorizontalScrollBarDecorator extends WindowDecorator {
    public HorizontalScrollBarDecorator (Window decoratedWindow) {
        super(decoratedWindow);
    }
 
    public void draw() {
        decoratedWindow.draw();
        drawHorizontalScrollBar();
    }
 
    private void drawHorizontalScrollBar() {
        // draw the horizontal scrollbar
    }
 
    public String getDescription() {
        return decoratedWindow.getDescription() + ", including horizontal scrollbars";
    }
}

여기 아래에는 완전히 꾸며진(fully decorated) Window객체를 생성하는 테스트 프로그램이고, 실행해보면 설명을 출력한다.

public class DecoratedWindowTest {
    public static void main(String[] args) {
        // create a decorated Window with horizontal and vertical scrollbars
        Window decoratedWindow = new HorizontalScrollBarDecorator (
                new VerticalScrollBarDecorator(new SimpleWindow()));
 
        // print the Window's description
        System.out.println(decoratedWindow.getDescription());
    }
}

이 프로그램의 결과는 "simple window, including vertical scrollbars, including horizontal scrollbars"라고 나올 것이다. 두 데코레이터의 getDescription 메서드에서 어떻게 처음에 꾸며진(decorated) Window의 설명(description)을 가져오고 그 뒤에 꾸미는 작업(decorates)을 하게되는지 유심히 살펴보아라.




두번째 예(커피 만드는 시나리오)

다음의 자바 예는 커피를 만드는 시나리오에서 데코레이터를 사용하는 방법을 설명하고 있다. 이 예에서 시나리오는 가격과 재료만 포함하고 있다.

// The Coffee Interface defines the functionality of Coffee implemented by decorator
public interface Coffee {
    public double getCost(); // returns the cost of the coffee
    public String getIngredients(); // returns the ingredients of the coffee
}
 
// implementation of a simple coffee without any extra ingredients
public class SimpleCoffee implements Coffee {
    public double getCost() {
        return 1;
    }
 
    public String getIngredients() {
        return "Coffee";
    }
}

다음의 클래스들에는 모든 커피 클래스가 포함되어 있다.(데코레이터 자신도 포함해서 말이다.)

// abstract decorator class - note that it implements Coffee interface
abstract public class CoffeeDecorator implements Coffee {
    protected final Coffee decoratedCoffee;
    protected String ingredientSeparator = ", ";
 
    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }
 
    public double getCost() { // implementing methods of the interface
        return decoratedCoffee.getCost();
    }
 
    public String getIngredients() {
        return decoratedCoffee.getIngredients();
    }
}
 
// Decorator Milk that mixes milk with coffee
// note it extends CoffeeDecorator
public class Milk extends CoffeeDecorator {
    public Milk(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
 
    public double getCost() { // overriding methods defined in the abstract superclass
        return super.getCost() + 0.5;
    }
 
    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Milk";
    }
}
 
// Decorator Whip that mixes whip with coffee
// note it extends CoffeeDecorator
public class Whip extends CoffeeDecorator {
    public Whip(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
 
    public double getCost() {
        return super.getCost() + 0.7;
    }
 
    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Whip";
    }
}
 
// Decorator Sprinkles that mixes sprinkles with coffee
// note it extends CoffeeDecorator
public class Sprinkles extends CoffeeDecorator {
    public Sprinkles(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }
 
    public double getCost() {
        return super.getCost() + 0.2;
    }
 
    public String getIngredients() {
        return super.getIngredients() + ingredientSeparator + "Sprinkles";
    }
}

아래에는 완전히 꾸며진(fully decorated) Coffee의 객체를 생성하는 테스트 프로그램이고, 여기에서는 커피의 가격을 계산하고 재료를 출력한다.

public class Main
{
    public static void main(String[] args)
    {
        Coffee c = new SimpleCoffee();
        System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
 
        c = new Milk(c);
        System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
 
        c = new Sprinkles(c);
        System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
 
        c = new Whip(c);
        System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
 
        // Note that you can also stack more than one decorator of the same type
        c = new Sprinkles(c);
        System.out.println("Cost: " + c.getCost() + "; Ingredients: " + c.getIngredients());
    }
}

이 프로그램의 출력 결과는 다음과 같다.

Cost: 1.0; Ingredients: Coffee
Cost: 1.5; Ingredients: Coffee, Milk
Cost: 1.7; Ingredients: Coffee, Milk, Sprinkles
Cost: 2.4; Ingredients: Coffee, Milk, Sprinkles, Whip
Cost: 2.6; Ingredients: Coffee, Milk, Sprinkles, Whip, Sprinkles



동적인 언어들(Dynamic languages)

데코레이터 패턴은 동적인 언어들에서도 사용될 수 있는데, 이때에는 인터페이스와 전통적인 OOP 상속을 사용하지 않고 쓴다.

자바스크립트(커피 만드는 시나리오)

// Class to be decorated
function Coffee() {
 
}
 
Coffee.prototype.cost = function() {
        return 1;
};
 
// Decorator A
function Milk(coffee) {
        var currentCost = coffee.cost();
        coffee.cost = function() {
                return currentCost + 0.5;
        };
 
        return coffee;
}
 
// Decorator B
function Whip(coffee) {
        var currentCost = coffee.cost();
        coffee.cost = function() {
                return currentCost + 0.7;
        };
 
        return coffee;
}
 
// Decorator C
function Sprinkles(coffee) {
        var currentCost = coffee.cost();
        coffee.cost = function() {
                return currentCost + 0.2;
        };
 
        return coffee;
}
 
// Here's one way of using it
var coffee = new Milk(new Whip(new Sprinkles(new Coffee())));
alert( coffee.cost() );
 
// Here's another
var coffee = new Coffee();
coffee = new Sprinkles(coffee);
coffee = new Whip(coffee);
coffee = new Milk(coffee);
alert(coffee.cost());


저작자 표시 비영리 변경 금지
신고
Posted by Code-Moon

댓글을 달아 주세요

티스토리 툴바