데코레이터 패턴이 무엇인지 알아보고 예제를 통해 이해하고자 합니다.
목표
- 데코레이터 패턴 이해
- 사용의 장단점
- 데코레이터 패턴 예제
데코레이터 패턴
데코레이터 패턴은 어떤 객체에 책임을 덧붙이는 패턴으로,
기능 확장이 필요할 때 기존 코드를 변경하지 않고 부가 기능을 동적으로(유연하게) 추가할 수 있도록 하는 패턴 입니다.
서브 클래스 대신 쓸 수 있는 유연한 대안이 됩니다.
예시
'에스프레소'라는 베이스를 만드는 머신기가 있는데,
고객들이 머신기에서 라떼와 모카, 바닐라 라떼도 만들수 있도록 해달라는 요청 사항이 들어옵니다.
- '에스프레소' 클래스를 상속받는 '우유' 클래스 -> 라떼
- '에스프레소' 클래스를 상속받는 '모카 시럽' 클래스 -> 모카 라떼
- '에스프레소' 클래스를 상속받는 '바닐라 시럽' 클래스 -> 바닐라 라떼
이렇게 상속을 통해 클래스를 만들 수 있지만
추후에는 휘핑크림 얹은 바닐라 라떼, 휘핑크림 얹은 모카 라떼와 같이 기존 메뉴들과 비슷하지만 또 다른 메뉴를 요청한다면?
- '라떼' 클래스와 '모카 시럽' 클래스 상속받는 '휘핑크림 얹은 모카 라떼' 클래스 -> 다중 상속 불가!!!!
라떼를 만들고 모카시럽을 추가하고 휘핑크림을 얹는 기능의 클래스를 생성해야 합니다.LatteWithMochaSyrupAndWhippingCream
클래스가 되겠습니다. - 휘핑크림 얹은 바닐라 라떼는
LatteWithBanillaSyrupAndWhippingCream
클래스가 됩니다.
이러한 방식은 새 메뉴 요청이 올 때마다 여러 기능을 수행하는 서브 클래스들이 생성되게 됩니다.
이때 데코레이터 패턴을 사용할 수 있습니다.
모카 시럽을 넣는 기능의 데코레이터, 우유를 부어 라떼를 만드는 기능의 데코레이터를 추가하면
라떼, 모카를 만들 수 있으며
신메뉴가 필요할 때마다 기존 데코레이터를 이용하여 조합하거나, 필요한 데코레이터만 생성하여 조합하여 만들어냅니다.
구성
- Component : 객체의 베이스가 되는 역할. 최대한 필드들을 포함시키지 않고 가볍게 구성.
- ConcreteComponent : Component를 상속하며 객체의 기본적인 책임을 수행.
- Decorator : Component를 상속받으면서도 Component를 필드로 갖고 있는 구조.
- ConcreteDecorator : Component에 부가 기능을 추가할 데코레이터 구현체들.
동작
- 데코레이터라고 불리는 클래스는 기존 베이스 인터페이스(Component)를 상속합니다.
- 데코레이터는 베이스 객체에 기능을 덧붙이는 역할이므로 홀로 생성될 수 없습니다. (무조건 1개의 Component와 함께 생성될 수 있습니다.)
- 라떼 데코레이터는, 베이스 객체인 '에스프레소'를 만드는 메서드를 호출한 후 본인의 기능인 '우유를 붓는 역할'을 수행합니다.
데코레이터 패턴이 사용되면 유용한 시점은 아래 경우입니다.
- 기존 기능에서 부가 기능을 추가하고 싶은데, 상속을 통해 서브클래스를 계속 만드는 방법이 비효율적일 때
- 객체의 타입과 호출 가능한 메소드를 그대로 유지하면서 객체에 새로운 책임을 추가할 때
- 상속받은 하위 클래스를 사용하길 원하지만, 런타임 시에 부가 기능을 추가하고 싶을 때
- 객체를 동적으로 수정할 수 있도록 하고 싶을 때
반면, 단점으로 적용될 수 있는 부분도 있습니다.
- 비슷한 성질의 작은 클래스가 많이 만들어질 수 있다는 단점.
주의할 점
- Component는 장식을 추가할 베이스가 되는 역할이므로 작고 가볍게 정의하도록 합니다. 저장이 필요한 것들은 구현체인 ConcreteComponent에 저장합니다.
- 코드를 수정하지 않고도 준비된 Decorator을 조합해 기능을 추가할 수 있도록 사고해야 합니다.
코드로 구현
(참고) 데코레이터 패턴을 사용할 때는 DI가 적극 활용됩니다. 👍
Beverage
클래스가 Component가 되며 Americano
클래스가 ConcreteComponent입니다.Latte
클래스, Mocha
클래스, WhippingCream
클래스가 부가 기능을 수행하는 Decorator입니다.
Component 역할인 Beverage (추상 클래스)
/**
* Component 역할
*/
public abstract class Beverage {
public abstract void brewing();
}
Base 객체이자 Concrete Component인 Espresso (클래스)
/**
* ConcreteComponent : Component인 Beverage 상속
*/
public class Espresso extends Beverage {
@Override
public void brewing() {
System.out.println("make Espresso.");
}
}
데코레이터 클래스 (추상 클래스)
/**
* Component를 구현하는 데코레이터 클래스이지만 N개의 데코레이터들의 추상체 역할만 수행하는 abstract 클래스
*/
public abstract class Decorator extends Beverage {
private final Beverage beverage; // 또 다른 Component를 가지고 있어야 함.
public Decorator (Beverage beverage) {
this.beverage = beverage;
}
@Override
public void brewing() {
beverage.brewing(); // Component의 operation() 호출
}
}
우유를 붓는 기능의 데코레이터인 Latte (클래스)
/**
* Decorator 클래스1
* 상위 Decorator의 brewing()을 호출한 뒤 본인의 기능을 수행한다.
*/
public class Latte extends Decorator {
public Latte(Beverage beverage) {
super(beverage);
}
@Override
public void brewing() {
super.brewing();
System.out.println("pour Milk.");
}
}
모카 시럽을 붓는 기능의 데코레이터인 Mocha (클래스)
/**
* Decorator 클래스2
* 상위 Decorator의 brewing()을 호출한 뒤 본인의 기능을 수행한다.
*/
public class Mocha extends Decorator {
public Mocha(Beverage beverage) {
super(beverage);
}
@Override
public void brewing() {
super.brewing();
System.out.println("put Mocha Syrup.");
}
}
커피 머신기 (호출 클래스)
public class CoffeeMachine {
public void offer() {
System.out.println("===============Concrete Component : Espresso===============");
Espresso espresso = new Espresso();
espresso.brewing();
Espresso latte = new Latte(espresso); // 에스프레소에 우유 붓는 기능 추가하여 라떼 만듦.
latte.brewing();
Espresso mocha = new Mocha(latte); // 라떼에 모카시럽 덧붙여서 모카 라떼 만듦.
mocha.brewing();
System.out.println("================Concrete Decorator : Concrete Decorator인 Milk에 Mocha Syrup 추가==============");
Espresso mochaWithWhippingCream = new MochaWithWhippingCream(new Mocha(new Latte(new Espresso())));
mochaWithWhippingCream.brewing();
}
}
- 마지막에 휘핑크림 얹은 모카를 만드는 코드를 보면 알 수 있듯 Component 객체인 에스프레소가 무조건 먼저 생성되어야 합니다.
- 데코레이터 구현체의 실행 메서드
brewing()
에서는 상위 데코레이터의 실행 메서드brewing()
을 호출하고, 상위 데코레이터는 베이스 객체의 실행 메서드brewing()
을 호출합니다. -> 재귀적인 방식을 통해 실행
정리
결국, 객체에 동적으로 새로운 책임을 추가할 수 있게 합니다.
기능을 추가하는 방법은...
베이스가 되는 기능의 객체와 해당 기능에 부가 기능을 덧붙이는 데코레이터 객체를 동일한 컴포넌트에 대해 상속받도록 해서 동일한 레벨의 구현체로 존재하게 합니다. 그리고 베이스 객체를 데코레이터 객체로 감싸는 방식으로 베이스 객체에 원하는 조합을 추가할 수 있습니다.
이는 서브클래스를 생성하는 것보다 융통성 있는 방법으로 보입니다.
생각
현재 내가 개발하고 있는 사내 프로젝트에서는 데코레이터 패턴이 필요할 상황을 자주 접하지는 못할 것 같지만,
해당 패턴을 잘 이해하고 있으면 데코레이터 패턴이 필요한 상황에서 장단점을 고려할 수 있겠다.
참고한 글