데코레이이터 패턴은 이미 존재하고 있는 객체들에 대해서 dynamic wrapping 을 하여 그들의 [책임]을 변경 / 추가 하기 위해서 사용되는 패턴이다. 보통 동종 객체에서 다른 책임을 부여하기 위해서는 Inheritance 를 많이 사용하지만 Sub-Class 를 무분별하게 만들지 않고 책임지는 영역을 넓히기 위한 구조패턴이다.
해당 경우에서 각 구현체 안에서 동일한 특성들이 추가되어야 한다고 해보자. 가령, 각 커피에 휘핑크림이 들어가는지, 우유가 들어가는지, 모카가 들어가는지 등에 대한 속성들이 추가되어야 한다. 일반적으로는 Beverage 자체에 attribute 를 추가하는 방식을 생각할 것이다.
물론 틀린 것은 아니지만, 속성이 위와 같이 늘어날 때마다 계속 attribute 가 추가되는건 관리가 어려워지고, 총 가격 계산따위의 로직을 처리한다면 일일이 if 분기를 쳐줄 생각만 해도 아찔하다. 그렇다고 전략패턴처럼 일일이 구현체를 만들어준다면, HouseBlendMilk, HouseBlendSoy, HouseBlendMocha .... 수많은 Sub-Class 들이 등장해야한다.
OCP는 확장에 열려있어야 하지만, 변경시 수정해야 하는 과정을 최소화해야 한다는 원리였다. 하지만 위 상황처럼 polymorphism 을 과도하게 사용하면 유지 / 보수가 오히려 안좋아질 수도 있는 것이다 (모든 상황에 전략 패턴이 적절한게 아니라는 것을 보여준다).
데코레이터 패턴의 기본 아이디어는 다음과 같다. 위와 같이 속성들이 Root 객체를 둘러싸고 있다면, 전체 가격따위를 계산해야할 경우 밖으로 빼내면서 가격을 누적시키는 방식으로 계산할 수 있다. 이렇게 할 수 있는 기능(책임)을 늘릴 수 있다.
1. Decorator 는 여러번 Wrapping 을 할 수 있다
2. Wrapping 이 n 번 진행된 객체와 Wrapping 이 되지 않은 객체는 동일한 type 으로 다룰 수 있다
3. Wrapping 을 하는 과정을 Run-Time 시 동적으로 진행할 수 있다
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을 살펴보자.
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 을 봤을 때 전혀 알 수 없을 것 같다. 항상 실무를 할 때 설계 단계에서 이 디자인 패턴을 적용해보는데에 있어서 익숙해지는게 중요할 것 같다.
출처
1) 에릭 프리먼, 『Head First Design Patterns』, O'Reilly Media (2004)
2) 중앙대학교 소프트웨어학부 이찬근 교수님 디자인 패턴 수업자료 중
'SW 설계 > Design Pattern' 카테고리의 다른 글
[Design Pattern] Structural - 컴포지트 패턴 (Composite Pattern) (0) | 2023.10.22 |
---|---|
[Design Pattern] Structural - 아답터 패턴 (Adapter Pattern) (0) | 2023.10.22 |
[Design Pattern] Creational - 싱글톤 패턴 (Singleton Pattern) (1) | 2023.10.21 |
[Design Pattern] Creational - 빌더 패턴 (Builder Pattern) (0) | 2023.10.21 |
[Design Pattern] Creational - 팩토리 메소드 패턴 (Factory Method Pattern) (0) | 2023.10.20 |