데코레이터(Decorator)란 의미는 '장식자'라는 의미로서 어떤 객체를 꾸며준다? 추가한다? 이러한 의미라고 할 수 있다. 따라서 데코레이터 패턴은 대상 객체에 대한 기능 확장이나 변경이 필요할 때 객체에 새로운 요소(행동 or 기능)을 동적으로 더할 수 있는 패턴을 말한다.
데코레이터 패턴을 사용하면 서브클래스(상속)을 통한 확장보다 훨씬 유연한 확장을 할 수 있다.
출처 : https://refactoring.guru/ko/design-patterns/decorator
데이코레이터 패턴의 구조는 위와 같다.
모두의 슈퍼 클래스로서 Wrapper(Base Decorator)들과 래핑되는 객체 (Concrete Components of Component)들 모두에 대한 공통 인터페이스를 선언한다.
public interface Component {
void execute();
}
Component의 구현체로, 기본 행동(Default 기능)을 구현한다. 데코레이터 구현체에게 포장되는 객체(wrappee)를 상징한다.
public class ConcreteComponent implements Component {
@Override
public void execute() {
System.out.println("ConcreteComponent execute(default behavior)");
}
}
기초 데코레이터로서 래핑되는 객체를 참조하기 위한 필드가 존재한다. 필드으 타입은 구상 컴포넌트와 데코레이터를 모두 포함할 수 있는 Component 인터페이스 타입으로 선언한다.
public abstract class BaseDecorator implements Component {
private final Component wrappee;
public BaseDecorator(Component component) {
this.wrappee = component;
}
@Override
public void execute() {
wrappee.execute();
}
}
BaseDocorators의 구현체로서 컴포넌트에 동적으로 추가될 수 있는 추가 행동(기능)들을 정의하는 클래스이다. 구현 데코레이터는 수퍼 클래스의 추상 메서드(excute)에 추가 요소(확장을 위한 추가 기능 or 행동)에 대한 부분을 정의한다.
public class ConcreteDecorator1 extends BaseDecorator {
public ConcreteDecorator1(Component component) {
super(component);
}
@Override
public void execute() {
super.execute();
extra();
}
private void extra(){
System.out.println("ConcreteDecorator1 execute");
}
}
@Test
void 데코레이터_패턴_테스트() {
Component concreteComponent = new ConcreteComponent();
Component concreteDecorator1 = new ConcreteDecorator1(concreteComponent);
concreteDecorator1.execute();
Component concreteDecorator2 = new ConcreteDecorator2(concreteComponent);
concreteDecorator2.execute();
Component concreteDecorator1And2 = new ConcreteDecorator1(new ConcreteDecorator2(concreteComponent));
concreteDecorator1And2.execute();
Component concreteDecorator2And1 = new ConcreteDecorator2(new ConcreteDecorator1(concreteComponent));
concreteDecorator2And1.execute();
}
이렇게 기본 행동을 정의한 Concrete Component를 생성하여 데코레이터 구현체에 생성자로 넣어서 데코레이터를 생성하게 된다면 기본 행동에 데코레이터의 기능까지 추가하여 동적으로 확장할 수 있다.
위의 코드 실행시
ConcreteComponent execute(default behavior)
ConcreteDecorator1 execute
ConcreteComponent execute(default behavior)
ConcreteDecorator2 execute
ConcreteComponent execute(default behavior)
ConcreteDecorator2 execute
ConcreteDecorator1 execute
ConcreteComponent execute(default behavior)
ConcreteDecorator1 execute
ConcreteDecorator2 execute
이렇게 출력되는데 여기서 중요시 볼 부분이 데코레이터 순서에 따라 실행 데코레이터의 행동 순서가 정해지기 때문에 여러 데코레이터를 사용할 시에는 순서가 중요하다는 것을 알 수 있다.
데코레이터 패턴에는 여러 특징이 존재한다.
데코레이터의 슈퍼 클래스는 자신이 자식하고 있는 슈퍼 클래스와 같다. 즉, 데코레이터의 최상단 부모는 Component이다.
한 객체를 여러 개의 데코레이터로 장식할 수 있다.
데코레이터는 자신이 장식하고 있는 객체
(Component, wrappe)에게 어떤 행동을 위임하는 일 말고도 추가 작업을 추가 및 수행할 수 있다. 어떤 행동을 위임하나다는 말은 데코레이터만이 가지고 있는 특정한 메서드를 호출할 수 있다는 의미이다.
서브클래스와 다르게 작은 단위로 기능을 구현한 데코레이터 집합체와 구성요소를 런타임 시점에 적절하게 조합이 가능하기 때문에 다양한 기능을 만들어낼 수 있는 유연성을 지닌다. 따라서 이는 코드를 수정하지 않고 동적으로 확장이 가능하다는 의미로서 OCP 원칙에 위반되지 않는다.
커피라는 Component에 Latte라는 구현체가 있다고 해보자. 라떼에는 다양한 토핑이 추가될 수도 있고 다양한 컵 종류로 담아서 만들 수 있다. 이를 코드로 구현해보자.
public interface Coffee {
void makeCoffee();
}
public class Latte implements Coffee {
@Override
public void makeCoffee() {
System.out.println("Latte");
}
}
public abstract class Topping implements Coffee {
protected final Coffee wrappee;
protected Topping(Coffee wrappee) {
this.wrappee = wrappee;
}
@Override
public void makeCoffee() {
wrappee.makeCoffee();
addTopping();
}
protected abstract void addTopping();
}
public class CreamTopping extends Topping {
public CreamTopping(Coffee coffee) {
super(coffee);
}
@Override
protected void addTopping() {
System.out.println("Cream Topping");
}
}
public abstract class Cup implements Coffee {
protected final Coffee wrappee;
protected Cup(Coffee wrappee) {
this.wrappee = wrappee;
}
@Override
public void makeCoffee() {
wrappee.makeCoffee();
addCup();
}
protected abstract void addCup();
}
public class RecycleCup extends Cup {
public RecycleCup(Coffee wrappee) {
super(wrappee);
}
@Override
protected void addCup() {
System.out.println("Recycle Cup");
}
}
이를 client가 조합하여 실행해보자
@Test
void 커피_데코레터_패턴_적용(){
Coffee latte = new Latte();
Coffee creamToppingLatte = new CreamTopping(latte);
creamToppingLatte.makeCoffee();
System.out.println();
Coffee recycleCupLatte = new RecycleCup(latte);
recycleCupLatte.makeCoffee();
System.out.println();
Coffee creamAndChocolateToppingLatte = new CreamTopping(new RecycleCup(latte));
creamAndChocolateToppingLatte.makeCoffee();
}
//결과
Latte
Cream Topping
Latte
Recycle Cup
Latte
Recycle Cup
Cream Topping
이렇게 결과가 출력되었다. 그러면 이것이 어떻게 작동하게 되는 것일까?
위의 그림과 예시코드를 합쳐서 설명해보자. 마지막 결과를 보면 new CreamTopping(RecycleCup(latte) 이렇게 생성하였는데 실행되는 것은 RecycleCup이 먼저이다. 이를 통해 알 수 있는 것은 마지막 데코레이터의 추가 행동이 먼저 실행됨을 알 수 있다.
이를 순서대로 알아보면
데코레이터 패턴은 기존의 기능에서 추가적으로 기능이 추가 되거나 추가 행위가 필요할 경우 사용하면 된다. 예시를 들어보자. 만약 내가 신문사를 다니고 있고 신문 기사가 발행됬음을 알리는 기능을 개발했다고 가정하자. 기존에는 카카오톡으로만 알림기능이 갔다. 하지만 추후에 페이스북이나 인스타그램에도 알림을 보내고 싶다고 하면 데코레이터 페턴을 이용하여 페이스북, 인스타그램 데코레이터를 생성하여 추가 기능을 사용한다면 기존의 기능을 수정하지 않고 새로운 기능만 추가하여 확장이 가능해 질 것이다.
데코레이터 패턴의 가장 큰 장점은 서브클래스를 만들때보다 훨씬 유연하게 기능을 확장할 수 있다는 점이다. 또한 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다는 장점있다.
이러한 장점들은 객체지향개발 원칙에도 부합하는데, 각 데코레이터마다 고유의 책임이 있다는 점에서 단일책임의 원칙(SRP)를 준수하고 있다고할 수 있다. 클라이언트의 코드 수정 없이 동적으로 데코레이터를 추가하여 기능 확장이 가능하니 개방 폐쇄 원칙(OCP)에도 부합하다. 마지막으로 구현체가 아닌 인터페이스를 바라보기 때문에 의존 역적 원칙(DIP)를 준수하고 있다.
만약 장식 데코레이터 일부만을 제거하고 싶다면, Wrapper 스택에서 특정 wrapper를 제거하는 것을 어렵다. 또한 데코레이터를 조합하는 초기 생성 코드가 new A(new B(new C())) 이러한 형식이어서 가독성이 떨어질 수 있다. 또한 데코레이터의 추가 순서가 중요하기 때문에 순서가 중요한 작업에 구현하기가 힘들다는 단점이 있다.
Java의 I/O 메서드에서 InputStream, OutputStream, Reader, Writer의 생성자를 활용한 파일 I/O 랩퍼 부분은 데코레이터 패턴의 대표적인 예이다.
checkedXXX(), synchronizedXXX(), unmodifiabeXXX() 은 기존의 기능에 추가적인 행동을 할 수 있도록 해주는 메서드들로써 데코레이터 패턴이 적용된 예이다.
서블릿에서 제공해주는 Wrapper로 이 역시 일종의 데코레이터 패턴이라고 볼 수 있다. HttpServletRequestWrapper / HttpServletResponseWrapper가 제공하는 기능을 오버라이딩해서 부가적인 기능을 추가할 수 있다.
출처: https://inpa.tistory.com/entry/GOF-💠-데코레이터Decorator-패턴-제대로-배워보자#httpservletrequestwrapper_/_httpservletresponsewrapper [Inpa Dev 👨💻:티스토리]