본문 바로가기

Design Patterns/영문 위키(Wikipedia)

Decorator Pattern (데코레이터 패턴)

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


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




객체 지향 프로그래밍에서 데코레이터 패턴은 이미 존재하는 객체에 동적으로 행위(동작, 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());