본문 바로가기
Architecture & Engineering/Design Pattern

[GoF Design Pattern] 3. 데코레이터(Decorator) 패턴

by 신숭이 2023. 6. 18.

[GoF Design Pattern] 3. 데코레이터(Decorator) 패턴

 

데코레이터 패턴은 소위 말해 객체를 장식하는 패턴이다. 객체를 장식할 때 기존 코드는 수정하지 않고, 새로운 기능을 확장할 수 있도록 돕는 패턴이다. 여기서 장식이라는 것은 어떻게 수행하는 것인지 예시를 통해 살펴보자

[헤드퍼스트 디자인 패턴] 책에서는 다음과 같은 예시를 들었다.

커피에 새로운 첨가물을 추가하여 새로운 커피를 만들어내야 한다. 이때 첨가물의 종류에 따라 커피의 가격이 달라지도록 개발해야 한다. 
커피의 구성요소는 크게 [기본 커피]와 [첨가물] 이라고 하자. 기본 커피에는 다양한 첨가물이 여러 경우의 수로 추가될 수 있다.
예를 들어 Dark Roast(1달러)라는 기본 커피에 Whip(+0.3달러), Mocha(+0.09달러)라는 첨가물을 추가하면 최종적으로 그 커피의 가격(cost)은 1.39 달러이다.

여기서 첨가물과 기본 커피는 얼마든지 새로 추가될 수 있다. 어떻게 설계해야 좋을까?

 



안 좋은 설계 1 : 상속으로 구성한다.


cost 함수만 가진 부모(Beverage) 클래스를 기반으로 모든 커피의 종류를 만들어 각 첨가물이 포함된 커피별로 클래스를 만들면 어떻게 될까? 그럼 다음과 같이 기하급수적으로 많은 클래스가 탄생할 것이다. 

출처 : http://www.vincehuston.org/dp/real_demos.html


새로운 첨가물이 추가되면 수십여개의 클래스를 또 추가로 작성해야 한다. 굳이 더 따져보지 않아도 최악의 설계방식이다.

 


안 좋은 설계 2 : cost 함수에 각 첨가물 별로 조건문을 걸어 처리한다.


부모 클래스(Beverage)는 각 첨가물을 Boolean 변수로 가지고 기본 커피들은 부모클래스를 상속하고 생성할 때 해당 첨가물이 있으면 true 그렇지 않으면 false로 두어, 오버라이드한 cost 함수에서 최종적인 가격을 계산할 수 있도록 한다.

부모 클래스는 다음과 같을 것이다.

public class Beverage {
    double milkCost = 0.3;
    double soyCost = 0.09;
    double mochaCost = 0.1;
    double whipCost = 0.2;
    
    private boolean milk = false;
    private boolean soy = false;
    private boolean mocha = false;
    private boolean whip = false;

    public double cost() {
        double condimentCost = 0.0;
        if (milk) {
            condimentCost += milkCost;
        }
        if (mocha) {
            condimentCost += mochaCost;
        }
        if (soy) {
            condimentCost += soyCost;
        }
        if (whip) {
            condimentCost += whipCost;
        }

        return condimentCost;
    }

    void setMilk(boolean b) {
        milk = b;
    }

    void setSoy(boolean b) {
        soy = b;
    }

    void setMocha(boolean b) {
        mocha = b;
    }

    void setWhip(boolean b) {
        whip = b;
    }
}


서브 클래스는 다음과 같을 것이다. 기본 커피를 정의한 것으로 객체를 생성할  때 첨가물의 여부를 세터(Setter)로 설정할 수 있을 것이다.

public class DarkRoast extends Beverage {
    public double cost() {
        return 1.33+super.cost();
    }
}

 

이 설계 방식의 단점은 다음과 같다.

1. 첨가물이 추가될 때마다 Beverage 클래스가 수정되어야 한다.
2. 서브 클래스 방식은 객체의 행동이 컴파일 타임에 결정된다.
(= 런타임에 결정되지 않는다 = 실행 중에 결정되지 않는다 = 동적이지 않다 = 유연하지 않다 = 딱딱하다 )

이러한 단점을 극복하면서, 객체에 새로운 기능을 추가(장식)하는 방법이 있을까?

우리가 원하는 목표는 딱 기본 커피 클래스들과 첨가물 클래스 정도만 구현하여 각 클래스들을 재사용할 수 있도록 하고 새로운 첨가물이나 기본 커피를 추가하더라도 기존 코드를 고치지 않고자 하는 것이다. 이걸 가능케 하는 것이 데코레이터 패턴이다.

 

 

디자인 원칙 5 : 클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다. (OCP : Open-Closed Principle)

 

데코레이터 패턴은 OCP를 완전히 준수한다.

변경에 닫혀있다 : 데코레이터 패턴은 인터페이스에 맞춰서 시스템을 설계한다. (인터페이스에 맞춰 프로그래밍하는 것은 보통 변경에 닫혀있게 만든다. 즉 기존의 코드를 수정하면 시스템에 그 파급이 퍼져나간다. -> 오류의 유발가능성이 높다 -> 변경에 닫혀있다.)
확장에 열려있다 : 데코레이터 패턴은 기능의 추가로 시스템의 변경이 발생하여 클래스를 확장하고자할때, 기존의 코드를 아예 건드리지 않을 수 있다. (새로운 데코레이터의 추가가 용이하다)

 

 

데코레이터 패턴의 정의

 

객체에 추가 요소를 동적으로 더할 수 있다. 데코레이터를 사용하면 서브클래스를 만들 때 보다 유연하게 기능을 확장할 수 있다.



 

UML

 

데코레이터는 구조(Structural)패턴에 속한다. 주목할 점은 꾸며지는 대상(ConcreteComponent)과 데코레이터 추상클래스(Decorator) 모두 같은 인터페이스인 Componet를 구현하고 있다는 것이다.

https://www.codeproject.com/Tips/468951/Decorator-Design-Pattern-in-Java

 

이제 Beverage의 예시를 데코레이터 패턴에 맞춰 구현해보면 다음과 같다. 목적은 꾸며지는 대상과 장식만 클래스로 구현하는 것이고, 이들을 자유롭게 조합할 수 있으며, 새로운 클래스가 추가될 때 기존 클래스의 수정이 없어야 한다. 

우선 UML로 표현하면 다음과 같을 것이다. 본문 맨위에 있는 다이어그램에 비하면 클래스의 개수가 현저히 줄었고 딱 필요한 것들만 있다.

headfirst design pattern chapter 3 - decorator pattern

 

 

Example Code 

 

꾸며지는 클래스 (Beverage 구상 클래스)

public abstract class Beverage {
    String description = "Unknown Beverage";

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}


public class DarkRoast extends Beverage {
    public DarkRoast() {
        description = "Dark Roast Coffee";
    }

    public double cost() {
        return .99;
    }
}

public class Espresso extends Beverage {

    public Espresso() {
        description = "Espresso";
    }

    public double cost() {
        return 1.99;
    }
}

..

 

추상 데코레이터 클래스와 구상 데코레이터 : 데코레이터는 생성자에서 꾸밀 대상을 받는다.

public abstract class CondimentDecorator extends Beverage {
    Beverage beverage;
    public abstract String getDescription();
}

public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    public double cost() {
        return .10 + beverage.cost();
    }
}

public class Mocha extends CondimentDecorator {
    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    public String getDescription() {
        return beverage.getDescription() + ", Mocha";
    }

    public double cost() {
        return .20 + beverage.cost();
    }
}

..


이제 비즈니스 로직에서 어떻게 사용하는지 보자

public class StarbuzzCoffee {
 
	public static void main(String args[]) {
		Beverage beverage = new Espresso();
		System.out.println(beverage.getDescription() 
				+ " $" + beverage.cost());
 
		Beverage beverage2 = new DarkRoast();
		beverage2 = new Mocha(beverage2);
		beverage2 = new Mocha(beverage2);
		beverage2 = new Whip(beverage2);
		System.out.println(beverage2.getDescription() 
				+ " $" + beverage2.cost());
 
		Beverage beverage3 = new HouseBlend();
		beverage3 = new Soy(beverage3);
		beverage3 = new Mocha(beverage3);
		beverage3 = new Whip(beverage3);
		System.out.println(beverage3.getDescription() 
				+ " $" + beverage3.cost());
	}
}

 

이제 코드를 다시 곱씹어서 살펴보면, 새로운 데코레이터(첨가물의 종류)가 추가되거나 꾸며질 기본 베이스 커피가 추가하기 용이해졌다는 것을 확인할 수 있다. 기능이 확장되어도 기존 코드에 전혀 영향을 주지 않는 것이다. (OCP)

개발을 하다보면 특정 인터페이스가 데코레이터 패턴이라고 확인이 되면, 새로운 기능을 추가하는 것은 어려운 일이 아닐 것이다. (시스템을 충분히 이해했다는 가정하) 

데코레이터의 대표적인 예시가 java.io의 InputStream이다. 여기서 FilterInputStream은 추상 데코레이터이다.

http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document

 

여기서 모든 입력 스트림의 문자를 소문자로 바꿔주는 LoserCaseInputStream 이라는 데코레이터를 추가한다고 생각해 보자. 대충 감이 올 것이다. 

다만 데코레이터 패턴은 단점도 명확하다. 여전히 클래스가 많이 추가된다는 단점이 있고, 초기화 코드(생성자)가 시스템에 따라 매우 복잡해질 수 있다. 결국 관리할 객체가 늘어나는 것인데, 이는 실수를 유발하기 좋다. 

그래서 보통 팩토리(Factory) 패턴이나 빌더(Builder)패턴과 같이 적합한 생성형(Creational) 패턴들을 이용해, 데코레이터를 만들고 사용하는 편이다. 

 

 

댓글