데코레이터 패턴 (Decorator Pattern)
decorator 의 사전적 의미 : 장식, 꾸미다.
데코레이터 패턴의 다름 이름 : Wrapper Pattern
데코레이터 패턴(Decorator pattern)이란 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴으로,
기능 확장이 필요할 때 서브클래싱 대신 쓸 수 있는 유연한 대안이 될 수 있다.
객체들을 새로운 행동(기능)들을 포함한 특수 래퍼 객체들
내에 넣어서 위 행동들을 해당 객체들에 연결
시키는구조적 디자인 패턴
이다.
디자인패턴의 중요한점은 디자인패턴으로 해결하고자 하는 문제
이다
데코레이터 패턴으로 해결하려는 문제는 다음과 같다
GoF 책
객체에 동적으로 새로운 책임(기능)을 추가할 수 있게하려고 한다.
기능을 추가하려면 서브클래스를 생성하는 것보다 융통성 있는 방법을 데코레이터패턴으로 제공한다.
가끔 전체 클래스에 새로운 기능을 추가할 필요는 없지만, 개별적인 객체에 새로운 책임을 추가할 필요가 있다.
예를들어 어떤 인터페이스에만 스크롤링 같은 이벤트를 추가하거나, 로그를 추가할 수 있다.
이렇게 새로운 서비스의 추가가 필요할 때 이를 해결하는 일반적인 방법은 상속을 이용하는 것이다.
즉, 이미 존재하는 클래스를 상속받고, 또 다른 클래스에서 상속받아 구현할 수 있지만, 유용하지는 않다.
`상속이 정적이고, 구성요소를 언제, 그리고 어떻게 장식해야 할지 제어할 수 없기 때문이다.
더 나은 방법은 지금 필요한 기능을 추가하는 다른 객체에다가 해당 객체를 둘러싸는 것이다.
이렇게 무엇인가를 감싸는 객체를 장식자(decorator)라고 한다.
데코레이터는 자신이 둘러싼 요소, 구성요소가 갖는 인터페이스를 자신도 동일하게 제공하므로, 장식자의 존재는 이를 사용하는 사용자에게 감춰진다.
즉, 데코레이터는 자신이 둘러싼 구성요소로 전달되는 요청을
중간에 가로채서 해당 구성요소에 전달해 준다
(프록시).그렇기 때문에 이 전달 과정의 앞뒤에 다른 작업을 추가로 할 수 있다 .
여기에는 투명성이 존재하기 때문에 데코레이터의 중첩이 가능하며, 이를 통해 책임 추가를 무한정으로 할 수 있다.
위는 전반적인 데코레이터 패턴의 구조이다.
- 여기서 Component는 기본 기능을 담당하는 ConcreteComponent와 추가 기능을 담당하는 Decorator의 공통 기능을 정의한다.
- ConcreateDecorator은 Decorator의 하위 클래스로, Component의 기본 기능과 Decorator의 추가 기능을 모두 제공한다.
- 그래서 ConcreateDecorator는 ConcreteComponent에 대한 참조가 필요한데, 조합을 통해서 표현한다.
데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가한다
예제
우리는 메시지를 출력하고 메시지를 리턴하는 기능을 가진 클래스를 구현할것이다.
그런데, 요구사항이 추가가 되어 로그를 출력하는기능과 기능이 실행한 시간을 계산해야 하는 기능을 추가로 구현해야 한다.
이 때 데코레이터 패턴을 이용하면 유용하게 구현할 수 있다.
여기서 Component는 우리가 구현하려는 기능이고, 이 Component에 덧대서 Decorator를 만든다.
ConcreateComponent 은 우리가 구현한 구체적인 구성 요소이다.
interface Component {
String execute();
}
@Slf4j
class ConcreateComponent implements Component {
@Override
public String operation() {
return "message";
}
}
@Slf4j
public class DecoratorPatternClient { // Component를 사용하는 클라이언트
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
log.info("result={}", result);
}
}
이제 다음과 같이 로그를 출력하는 기능을 가진 데코레이터를 추가할 수 있다.
@Slf4j
public class LogDecorator implements Component {
private Component component;
public LogDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("LogDecorator 실행");
String result = component.operation();
log.info("LogDecorator 종료 ");
return result;
}
}
public class DecoratorRunner {
@Test
void logDecorator() {
Component realComponent = new RealComponent();
Component logDecorator = new LogDecorator(realComponent);
DecoratorPatternClient client = new
DecoratorPatternClient(logDecorator);
client.run();
}
}
결과
LogDecorator 실행
result = message
LogDecorator 종료
로그를 출력해주는 데코레이터를 구현했다. 다음 요구사항은 실행시간을 출력하는 기능이다
@Slf4j
public class TimeDecorator implements Component {
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
}
}
public class DecoratorRunner {
@Test
void timeAndLogDecorator() {
Component realComponent = new RealComponent();
Component logDecorator = new LogDecorator(realComponent);
Component TimeDecorator = new TimeDecorator(logDecoratr);
DecoratorPatternClient client = new decoratorPatternClient(TimeDecorator);
client.run();
}
}
결과
TimeDecorator 실행
LogDecorator 실행
result = message
LogDecorator 종료
TimeDecorator 종료 resultTime = 10ms
데코레이터 패턴을 사용하면 위와 같이 자기가 감싸고 있는 구성요소의기존 클래스의 코드를 수정하지 않고 메소드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장할 수 있다.
데코레이터 패턴을 아래와 같은 상황일 때 사용하면 좋다.
- 클래스의 요소들을 계속해서 수정을 하면서 사용하는 구조가 필요한 경우
- 상속을 사용하여 객체의 행동을 확장하는 것이 어색하거나 불가능할 때 사용
- 여러 요소들을 조합해서 사용하는 클래스 구조인 경우
- final 클래스의 경우 기존 행동들을 재사용할 수 있는 유일한 방법은 데코레이터 패턴을 사용하여 클래스를 자체 래퍼로 래핑하는 것
- 객체들을 사용하는 코드를 훼손하지 않으면서 런타임에 추가 행동들을 객체들에 할당할 수 있어야 할 때 사용.
- 탈부착 가능한 책임을 정의할 때 사용한다.
- 상속을 통해 서브클래스를 계속 만드는 방법이 비효율적일 때 사용한다.
- 특히 조합되는 경우의 수가 많으면 서브클래스 수가 폭발적으로 늘어날 수 있다.
상속 vs 구현? (추상클래스 vs 인터페이스)
객체의 동작을 변경해야 할 때 가장 먼저 고려되는 방법은 클래스의 확장이다.
그러나 상속에는 다음과 같은 단점이 있다.
- 상속은 정적이다. 런타임(실행시간) 때 기존 객체의 행동을 변경할 수 없다.
- 전체 객체를 다른 자식 클래스에서 생성된 다른 객체로만 바꿀 수 있다.
- 자식 클래스는 하나의 부모 클래스만 가질 수 있다.
이 단점들을 극복할 수 있는 방법은 상속 대신 집합관계 또는 합성(구현)하는것이다.
장단점
장점
- 새 자식 클래스를 만들지 않고 추상화를 통해 기존 기능(행동)을 조합 및 추가 할 수 있다.
- 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다. (책임들을 추가하거나 제거)
- 현재 Client가 인터페이스를 사용함으로써 DI원칙을 적용할 수 있다.
- 다양한 행동들의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있다. (단일 책임 원칙)
단점
- 데코레이터를 조합하는 코드(초기 설정 코드)가 복잡할 수 있다. -> 가독성이 떨어진다.
- 래퍼들의 스택에서 특정 래퍼를 제거하기가 어렵다. -> 계속해서 다른 데코레이터를 호출하니까.
다른 패턴과의 관계
- 어댑터는 기존 객체의 인터페이스를 변경하는 반면 데코레이터는 객체를 해당 객체의 인터페이스를 변경하지 않고 추가한다. 또한 데코레이터는 어댑터를 사용할 때는 불가능한 재귀적 합성(체이닝)을 지원한다.
- 어댑터는 다른 인터페이스를, 프록시는 같은 인터페이스를, 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공한다.
- 책임 연쇄 패턴과 데코레이터는 클래스 구조가 매우 유사하다다. 두 패턴 모두 실행을 일련의 객체들을 통해 전달할 때 재귀적인 합성에 의존하나, 몇 가지 결정적인 차이점이 있다.
- 책임 연쇄 패턴 핸들러들은 서로 독립적으로 임의의 작업을 실행할 수 있으며, 또한 해당 요청을 언제든지 더 이상 전달하지 않을 수 있습니다. 반면에 다양한 데코레이터들은 객체의 행동을 확장하며 동시에 이러한 행동을 기초 인터페이스와 일관되게 유지할 수 있습니다. 또한 데코레이터들은 요청의 흐름을 중단할 수 없습니다.
- 컴포지트(복합체) 패턴 및 데코레이터는 둘 다 구조 다이어그램이 유사다. 둘 다 재귀적인 합성에 의존하여 하나 또는 불특정 다수의 객체들을 정리하기 때문이다.그러나 패턴들은 서로 협력할 수도 있다: 데코레이터*를 사용하여 *복합체 패턴 트리의 특정 객체의 행동을 확장할 수 있다.
- 데코레이터는 복합체 패턴과 비슷하지만, 자식 컴포넌트가 하나만 있다. 또 다른 중요한 차이점은 데코레이터는 래핑된 객체에 추가 책임들을 추가하는 반면 복합체 패턴은 자신의 자식들의 결과를 '요약'하기만 한다.
- 데코레이터 및 복합체 패턴을 많이 사용하는 디자인들은 프로토타입을 사용하면 종종 이득을 볼 수 있다. 프로토타입 패턴을 적용하면 복잡한 구조들을 처음부터 다시 건축하는 대신 복제할 수 있기 때문이다.
- 데코레이터는 객체의 피부를 변경할 수 있고 전략 패턴은 객체의 내장을 변경할 수 있다고 비유할 수 있다.
- 데코레이터와 프록시의 구조는 비슷하나 이들의 의도는 매우 다르다.
- 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 한다.
- 이 두 패턴의 차이점은 프록시는 일반적으로 자체적으로 자신의 서비스 객체의 수명 주기를 관리하는 반면 데코레이터의 합성은 항상 클라이언트에 의해 제어된다는 점이다.
- 합성 원칙 : 다른 객체의 참조 필드를 얻는 방식으로 런타임시 동적으로 이루어짐
'디자인 패턴' 카테고리의 다른 글
브릿지 패턴(Bridge Pattern) (0) | 2022.12.04 |
---|---|
어댑터 패턴(Adapter Pattern) (0) | 2022.11.18 |