본문 바로가기

SW 설계/Design Pattern

[Design Pattern] Structural - 데코레이터 패턴 (Decorator Pattern)

728x90

데코레이이터 패턴은 이미 존재하고 있는 객체들에 대해서 dynamic wrapping 을 하여 그들의 [책임]을 변경 / 추가 하기 위해서 사용되는 패턴이다. 보통 동종 객체에서 다른 책임을 부여하기 위해서는 Inheritance 를 많이 사용하지만 Sub-Class 를 무분별하게 만들지 않고 책임지는 영역을 넓히기 위한 구조패턴이다.

 

 

abstract 한 음료의 구현체들은 매핑한 구조

 

 

해당 경우에서 각 구현체 안에서 동일한 특성들이 추가되어야 한다고 해보자. 가령, 각 커피에 휘핑크림이 들어가는지, 우유가 들어가는지, 모카가 들어가는지 등에 대한 속성들이 추가되어야 한다. 일반적으로는 Beverage 자체에 attribute 를 추가하는 방식을 생각할 것이다. 

 

 

무분별하게 늘어나는 attribute

 

 

물론 틀린 것은 아니지만, 속성이 위와 같이 늘어날 때마다 계속 attribute 가 추가되는건 관리가 어려워지고, 총 가격 계산따위의 로직을 처리한다면 일일이 if 분기를 쳐줄 생각만 해도 아찔하다. 그렇다고 전략패턴처럼 일일이 구현체를 만들어준다면, HouseBlendMilk, HouseBlendSoy, HouseBlendMocha .... 수많은 Sub-Class 들이 등장해야한다. 

 

 

OCP는 확장에 열려있어야 하지만, 변경시 수정해야 하는 과정을 최소화해야 한다는 원리였다. 하지만 위 상황처럼 polymorphism 을 과도하게 사용하면 유지 / 보수가 오히려 안좋아질 수도 있는 것이다 (모든 상황에 전략 패턴이 적절한게 아니라는 것을 보여준다).

 

 

Decorator Pattern 의 기본적인 모습

 

 

데코레이터 패턴의 기본 아이디어는 다음과 같다. 위와 같이 속성들이 Root 객체를 둘러싸고 있다면, 전체 가격따위를 계산해야할 경우 밖으로 빼내면서 가격을 누적시키는 방식으로 계산할 수 있다. 이렇게 할 수 있는 기능(책임)을 늘릴 수 있다.

 

 

1. Decorator 는 여러번 Wrapping 을 할 수 있다

2. Wrapping 이 n 번 진행된 객체와 Wrapping 이 되지 않은 객체는 동일한 type 으로 다룰 수 있다

3. Wrapping 을 하는 과정을 Run-Time 시 동적으로 진행할 수 있다

 

 

Decorator Pattern 의 일반적인 UML

 

 

Decorator Pattern 의 중요한 원칙 중 하나는 wrapping 하려는 객체의 Super Class 를 상속받은 Decorator 역할체를 만들어줘야 한다는 것이다. 즉, 한 Decorator 역할체는 하나의 Class 가족을 위해서만 사용될 수 있는 것이다. 또한, Decorator 는 직접 해당 Class 를 사용해야 하기 때문에 Composition 관계도 가지고 있어야 한다. 즉, 데코레이터 패턴에서는 Decorator 가 Decorate 하려는 Object 의 최상 Class 를 의존과 동시에 상속되어야 한다.

 

 

위 Coffee 예제에서 Decorate 하고 싶은 대상은 최상단 Beverage 역할체였다. 음료에 추가될 속성을 Condiment 로, CondimentDecorator 를 추가해볼 수 있다. 우선 Beverage Class 와 Sub Class 를 살펴보겠다 (모두 동일한 구성이므로 하나만 추가하겠다).

 

 

public abstract class Beverage {

    protected String desc = "BEVERAGE";

    public String getDesc() {
        return this.desc;
    }

    // Decorator Pattern 을 적용하여 기능을 수행할 수 있다
    public abstract double cost();
}

 

 

public class Espresso extends Beverage{

    public Espresso() {
        super.desc = "ESPRESSO";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}

 

 

Beverage 는 모두 각자 자신이 어떤 음료인지 반환할 수 있고, 자신의 가격을 반환할 수 있다. 이 Beverage 와 같은 자격으로 Beverage 에 dynamic wrapping 을 해주기 위해 CondimentDecorator 는 Beverage 의 Sub Class 로 매핑된다. 또한, dynamic wrap 하기 위해서 Beverage 객체 또한 의존하여 사용할 수 있도록 한다

 

 

/*
 Beverage 와 같은 자격으로 Beverage 를 Wrap 하게 된다
 */
public abstract class CondimentDecorator extends Beverage {

    // 자신이 Wrap 하려는 객체를 알고 있어야 동일한 기능을 연이어 수행시켜줄 수 있다
    protected Beverage beverage;

    public abstract String getDescription();

}

 

 

public class WhipCondiment extends CondimentDecorator {

    // 자신이 Wrap 하려는 객체를 알고 있어야 한다
    public WhipCondiment(Beverage beverage) {
        super.beverage = beverage;
    }

    // Decorate 하는 객체가 하려는 책임에 자신의 책임을 더해준다
    @Override
    public String getDescription() {
        return super.beverage.getDesc() + " , Mocha";
    }


    // Decorate 하는 객체가 하려는 책임에 자신의 책임을 더해준다
    @Override
    public double cost() {
        return super.beverage.cost() + 0.5;
    }
}

 

 

Super Class 가 할 수 있는 일을 그대로 상속받아서 자동으로 그 일을 Wrapping 받은 결과에 맞춰 수행할 수 있도록 해준다. 이제 다음과 같이 Client 입장에서 Test 한다면 동일한 Beverage 객체의 결과가 달라지는 모습을 볼 수 있다.

 

 

Beverage beverage = new Espresso();

System.out.println(beverage.getDesc());
System.out.println(beverage.cost() + "");

beverage = new WhipCondiment(beverage);

System.out.println(beverage.getDesc());
System.out.println(beverage.cost() + "");

beverage = new MochaCondiment(beverage);

System.out.println(beverage.getDesc());
System.out.println(beverage.cost() + "");


---- 출력결과

ESPRESSO
1.99
ESPRESSO , Whip
2.49
ESPRESSO , Whip, Mocha
2.89

 

 

Decorator Pattern 을 잘 사용한 예시는 바로 Java 에서 유명한 I/O 라이브러리이다. 우리가 항상 많이 쓰는 InputStream 이 형태에 따라 속성과 책임을 추가해나간 모습이였던 것이다. 다음과 같은 UML을 살펴보자. 

 

 

IO 가 Decorator 라는걸 알고 나면 필요에 따라 사용할 수 있는 확장성이 넓어진다

 

 

FilterInputStream.java 는 Decorator 로, 해당 Class 의 하위 객체들을 Root Concrete 객체에 Wrapping 해주면서 기능을 확장시킬 수 있다. 가령, FileInputStream 을 사용하면서 Buffer 형태로 읽고싶거나, 읽지 않은 것처럼 Pushback 형태로 읽고 싶다면, 다음과 같이 사용해볼 수 있을 것이다. 

 

 

InputSteram is = new FileInputStream();
is = new BufferedInputStream(is);
is = new PushbackInputStream(is);

or

// 이런 형태가 Wrapping 의 개념인걸 잘 몰랐던 것 같다
InputStream is = new PushbackInputStream(new BufferedInputStream(new FileInputStream()));

 

 

Decorator Pattern 과 동일한 line 의 structural 디자인 패턴인 Proxy, Adapter Pattern 들은 다소 차이가 있다. 

 

 

Adapter 패턴은 대상 객체에게 서로 다른 interface 를 제공한다 

Proxy 패턴은 대상 객체에게 동일한 Level 의 interface 를 제공한다

Decorator 패턴은 대상 객체에게 강화된 interface 를 제공한다

 

 

 

정리하며

 

 

데코레이터 패턴은 기존 Root 객체 가족에게 추가적인 책임을 부여할 수 있는 OCP 가 돋보이는 중요한 패턴이다. 사용이 간단하고 OCP 를 잘 지킨만큼 유지 / 보수에서 이점을 가져간다 (맨 위에서 필요성을 알 수 있다). 

 

 

데코레이터 패턴의 핵심은 Decorator 역할체는 Root 객체를 상속받으면서 의존하여야 한다는 점이며, Root Concrete 객체들이 수행해야 하는 일들을 Decorator Concrete 객체들이 자신의 일을 얹으면서 대신 수행해주는 Delegation & Composition 이 돋보이는 패턴이였다. 

 

 

실무에서 이렇게 다양한 디자인 패턴들을 잘 사용하려면 정말 어려울 것 같다. 이런 패턴을 모른다면 위 UML 을 봤을 때 전혀 알 수 없을 것 같다. 항상 실무를 할 때 설계 단계에서 이 디자인 패턴을 적용해보는데에 있어서 익숙해지는게 중요할 것 같다. 

 

 

Decorate 된 상태도 동일한 Root 객체이다

 

 

 

출처

 

 

1) 에릭 프리먼, Head First Design Patterns』, O'Reilly Media (2004)

 

 

2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중

728x90