반응형
의도
- 객체에 동적으로 새로운 책임을 추가할 수 있게 한다. 기능을 추가하려면, 서브클래스를 생성하는 것보다 융통성 있는 방법을 제공한다.
동기
- 전체 클래스에 새로운 기능으르 추가할 필요는 없지만, 개별적인 객체에 새로운 책임을 추가하는 경우 일반적인 방법으로 상속을 이용한다. 그러나 이 방법은 별로 유용하지 않다.
- 예를 들어, 특정 요소에 테두리와 같은 속성을 추가한다고 하면 상속을 통해 서브클래스를 만들어 이 서브클래스의 인스턴스에 테두리가 있도록 할 수 있다. 그러나 테두리의 선택이 정적이다. 특정 요소에 해당하는 것만 테두리를 가진다.
- 사용자는 구성요소를 언제, 그리고 어떻게 테두리로 장식해야 할 지 제어할 수 없다.
- 더 나은 방법은 지금 필요한 테두리를 추가하는 다른 객체에다가 해당 구성요소를 둘러싸는 것이다.
- 테두리가 필요한 요소는 해당 객체로 감싸면 테두리를 제공할 수 있다. 즉, 테두리의 선택이 동적으로 변하며 구성요소에 사용자가 원하는 시점에 테두리를 적용할 수 있다.
- 이렇게 무엇인가를 감싸는 객체를 장식자(decorator)라고 한다.
- 장식자는 자신이 둘러싼 요소, 구성요소가 갖는 인터페이스를 자신도 동일하게 제공하므로, 장식자의 존재는 이를 사용하는 사용자에게 감춰진다.
- 장식자는 자신이 둘러싼 구성요소로 전달되는 요청을 중간에 가로채서 해당 구성요소에 전달한다.
- 이 때, 장식자에서 정의한 추가 기능을 장식자 본인이 직접 호출한다.
- 복합 방법
활용성
- 동적으로 투명하게(transparent). 즉, 다른 객체에 영향을 주지 않고 객체에 새로운 책임을 추가하기 위해 사용한다.
- 제거될 수 있는 책임에 대해 사용한다.
- 실제 상속으로 서브클래스를 계속 만드는 방법이 실질적이지 못할 때 사용한다.
- 많은 수의 독립된 확장이 가능할 때 모든 조합을 지원하기 위해 이를 상속으로 해결하면 클래스의 수가 폭발적으로 많아진다.
결과
- 단순한 상속보다 설계의 융통성을 더 많이 증대시킬 수 있다.
- 장식자를 사용하면 장식자를 객체와 연결하거나 분리하는 작업을 통해 새로운 책임을 추가하거나 삭제하는 일이 런타임에 가능해진다. 또한 장식자의 조합을 통해서 새로운 책임도 조합할 수 있다.
- Component 클래스와 Decorator 클래스 사이는 집합 관계이다.
- Decorator는 Component 클래스의 인스턴스를 포함할 수 있는데 이는 Decorator의 서브클래스를 코드의 수정 없이 포함할 수 있다는 의미이다. Decorator 클래스의 서브클래스 여러 개를 포함하면 여러 책임을 조합하는 결과가 된다.
- 상속은 정적으로 새로운 클래스를 추가해야만 추가적인 행동을 정의할 수 있는 방법이다.
- 새로운 기능이 추가될 때마다 클래스의 수가 증가한다.
- 장식자를 사용하면 장식자를 객체와 연결하거나 분리하는 작업을 통해 새로운 책임을 추가하거나 삭제하는 일이 런타임에 가능해진다. 또한 장식자의 조합을 통해서 새로운 책임도 조합할 수 있다.
- 클래스 계통의 상부측 클래스에 많은 기능이 누적되는 상황을 피할 수 있다.
- 장식자 패턴은 책임 추가 작업에서 필요한 비용만 그때 지불하는 방법을 제공한다.
- 예상하지 못한 특성들을 한꺼번에 다 개발하기 위해 고민하고 노력하기보다는 발견하지 못하고 누락된 서비스들을 Decorator 객체를 통해 지속적으로 추가할 수 있다.
- 기능은 단순한 구성요소들의 조합으로 얻어질 수도 있기 때문에 개발 시, 현재 사용되지 않은 기능까지 개발하기 위해 시간과 노력을 투자할 필요는 없다. 추후 기능이 필요하면 새로운 종류의 Decorator를 개발하면 된다.
- 장식자와 해당 장식자의 구성요소가 동일한 것은 아니다.
- 장식자는 사용자에게 일관된 인터페이스를 제공하는 껍데기이다. 그러므로 객체 식별자 관점에서 구성요소와 이를 둘러싼 Decorator 객체가 동일한 식별자를 가질 필요는 없다.
- 장식자를 사용함으로써 작은 규모의 객체들이 많이 생긴다.
- 장식자 패턴을 사용하는 설계에서는 규모가 작은 객체들의 수가 많아지는데, 이 객체들이 서로 다른 점은 상호작용하는 방법에 있을 뿐, 클래스가 다르거나 변수에 정의된 값이 다른 것은 아니다. 클래스들이 어떻게 조합하여 새로운 모습과 기능을 만들어 내는가에 따라서 새로운 객체가 계속 만들어지기 때문이다.
- 이때 이 객체들을 잘 이해하고 있다면 시스템의 재정의가 쉽겠지만, 그렇지 않다면 객체들을 모두 이해하고 수정하는 과정이 복잡해진다.
협력 방법
- Decorator는 자신의 Component 객체 쪽으로 요청을 전달한다. 요청 전달 전 및 전달 후에 자신만의 추가 연산을 선택적으로 수행할 수도 있다.
구현 고려사항
- 인터페이스 일치시키기
- Decorator 객체의 인터페이스는 반드시 자신을 둘러싼 구성요소의 인터페이스를 만족해야 한다. 따라서 ConcreteDecorator 클래스는 동일한 부모 클래스를 상속해야 한다.
- 추상 클래스로 정의되는 Decorator 클래스 생략하기
- Decorator 클래스에 정의할 책임이 한 가지 밖에 존재하지 않는 경우 추상 클래스인 Decorator 클래스를 정의할 필요가 없다. 이는 새로운 클래스를 설계할 때 발생하기보다는 기존에 존재하는 클래스 계통을 사용해야 할 때 자주 일어난다.
- 추상 클래스를 정의하지 않는 경우, 구성요소에게 요청을 전달하는 Decorator 클래스의 책임을 ConcreteDecorator와 합칠 수 있다.
- Component 클래스는 가벼운 무게를 유지하기
- 인터페이스를 만족하는지 확인하려면 Decorator와 구성요소 모두 동일한 부모 클래스인 Component 클래스를 상속받아야 한다.
- 가볍게 정의한다는 의미는 연산에 해당하는 인터페이스만을 정의하고 무언가 저장할 수 있는 변수는 정의하지 말라는 의미이다. 이렇게 하지 않으면 Component가 복잡해지고 이를 상속받는 여러 Decorator들도 복잡하고 무거운 클래스가 되어버린다.
- 데이터 저장소를 정의하는 것은 서브클래스에서 할 일이다.
- 또한 Component 클래스에 많은 서비스를 정의하는 것 또한 서브클래스에 부담이 된다.
- 각 서브클래스는 자신에게 필요 없는 연산까지도 적절한 구현을 해야하는 추가 부담이 생긴다.
- 객체의 겉포장을 변경할 것인가, 속을 변경할 것인가
- 장식자 패턴은 행동을 변경할 수 있도록 객체에 외장을 입힌 것이다.
- 내부를 변경하는 패턴은 전략 패턴과 관련이 있다.
- Component 클래스가 본질적으로 매우 복잡하고 무거운 특성을 갖는다면 전략 패턴이 더 나은 해결 방안이다.
구조
실제 구현 구조
소스코드
//Component
public interface PizzaPlace {
String createPizza();
int calculate();
}
//ConcreteComponent
public class CheesePizza implements PizzaPlace {
@Override
public String createPizza() {
return "Cheese Pizza";
}
@Override
public int calculate() {
return 10000;
}
}
//Decorator
public abstract class PizzaDecorator implements PizzaPlace {
private final PizzaPlace pizzaPlace;
protected PizzaDecorator(PizzaPlace pizzaPlace) {
this.pizzaPlace = pizzaPlace;
}
@Override
public String createPizza() {
return pizzaPlace.createPizza();
}
@Override
public int calculate() {
return pizzaPlace.calculate();
}
}
//ConcreteDecoratorA
public class HotSauceDecorator extends PizzaDecorator {
protected HotSauceDecorator(PizzaPlace pizzaPlace) {
super(pizzaPlace);
}
@Override
public String createPizza() {
return super.createPizza() + addHotSauce();
}
@Override
public int calculate() {
return super.calculate() + 1000;
}
//Component에 정의된 연산만을 인터페이스로 바라보고 요청하기 위해서 private로 설정
private String addHotSauce() {
return " + Hot Sauce";
}
}
//ConcreteDecoratorB
public class CheesePowderDecorator extends PizzaDecorator {
protected CheesePowderDecorator(PizzaPlace pizzaPlace) {
super(pizzaPlace);
}
@Override
public String createPizza() {
return super.createPizza() + addCheesePowder();
}
@Override
public int calculate() {
return super.calculate() + 2000;
}
//Component에 정의된 연산만을 인터페이스로 바라보고 요청하기 위해서 private로 설정
private String addCheesePowder() {
return " + Cheese Powder";
}
}
public class Main {
public static void main(String[] args) {
//기존에 만들어져 있는 cheese pizza
PizzaPlace cheesePizza = new CheesePizza();
System.out.println(cheesePizza.createPizza());
System.out.println("금액 : " + cheesePizza.calculate() + "원");
//Decorator를 사용하여 cheese powder를 뿌린 cheese pizza 생성
//사용자는 장식자의 존재 자체를 알 수 없다. 사용자는 Component 인터페이스를 통해서 자신의 정보를 접근하기 때문이며 장식자는 해당 인터페이스를 만족한다. 해당 인터페이스는 장식자에서 구체적으로 어떤 기능이 추가되는지는 모른다.
//장식자에서 실제로 추가되는 기능(addCheesePowder())은 사용자가 인터페이스를 통해서 요청한 것을 장식자가 요청을 가로챈 후에 장식자 본인이 직접 해당 기능을 호출한다.
PizzaPlace cheesePowderDecorator = new CheesePowderDecorator(cheesePizza);
System.out.println(cheesePowderDecorator.createPizza());
System.out.println("금액 : " + cheesePowderDecorator.calculate() + "원");
//Decorator를 사용하여 hot sauce를 뿌린 cheese pizza 생성
//사용자는 장식자의 존재 자체를 알 수 없다. 사용자는 Component 인터페이스를 통해서 자신의 정보를 접근하기 때문이며 장식자는 해당 인터페이스를 만족한다. 해당 인터페이스는 장식자에서 구체적으로 어떤 기능이 추가되는지는 모른다.
//장식자에서 실제로 추가되는 기능(addHotSauce())은 사용자가 인터페이스를 통해서 요청한 것을 장식자가 요청을 가로챈 후에 장식자 본인이 직접 해당 기능을 호출한다.
PizzaPlace hotSauceDecorator = new HotSauceDecorator(cheesePizza);
System.out.println(hotSauceDecorator.createPizza());
System.out.println("금액 : " + hotSauceDecorator.calculate() + "원");
//Decorator의 조합으로 hot sauce와 cheese powder를 모두 뿌리는 새로운 기능 획득. 새로운 cheese pizza 생성
PizzaPlace hotSauceCheesePowderDecorator = new HotSauceDecorator(new CheesePowderDecorator(cheesePizza));
System.out.println(hotSauceCheesePowderDecorator.createPizza());
System.out.println("금액 : " + hotSauceCheesePowderDecorator.calculate() + "원");
//hot sauce 두 번 뿌린 cheese pizza
PizzaPlace doubleHotSauceDecorator = new HotSauceDecorator(new HotSauceDecorator(cheesePizza));
System.out.println(doubleHotSauceDecorator.createPizza());
System.out.println("금액 : " + doubleHotSauceDecorator.calculate() + "원");
}
}
결과
관련 패턴
- 장식자 패턴은 적응자 패턴과 관련되어 있다.
- 원래의 적응자는 인터페이스를 변경시켜주는 것이었지만, 장식자는 객체의 책임, 행동을 변화시킨다.
- 복합체 패턴과 관련되어 있다.
- 장식자는 한 구성요소만들 갖는 복합체로 볼 수 있다. 그러나 이 목적은 객체의 합성이 아니라 객체에 새로운 행동을 추가하기 위한 것이다.
- 전략 패턴과 관련되어 있다.
- 장식자는 객체의 겉모양을 변경하고, 전략은 객체의 내부를 변화시킨다. 객체를 변경하는 두 가지 다른 대안인 셈이다.
[참고자료]
리처드 헬름, 랄프 존슨, 존 블리시디스, 『GoF의 디자인 패턴 : 재사용성을 지닌 객체지향 소프트웨어의 핵심요소』, 김정아 번역, 프로텍미디어(2015)
반응형