데코레이터 패턴
GoF의 디자인패턴 중 구조의 데코레이터 패턴을 Java 로 정리한 글
1. 데코레이터 패턴(Decorator Pattern)이란?
기존 코드를 변경하지 않고 부가 기능을 동적으로(유연하게) 추가하는 패턴
상속이 아닌 위임을 사용해서 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하다. 기능 확장이 필요할 때 상속대신 사용할 수 있는 패턴.
1-1. 구조
Component (Interface)
ConcreteComponent와 Decorator가 이를 구현하고 있다. 같은 오퍼레이션을 가지고 있다.
Decorator
컴포짓 패턴의 Composite과 비슷해보이지만 차이점이 있다.
Decorator는 단 하나의
wrappee
라고 하는 Component 타입의 인스턴스 타입을 가지고 있다.자신이 감싸고 있는 하나의 Component를 (오퍼레이션 내부에서) 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.
1-2. 언제 활용될까
상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
객체의 결합을 통해 기능이 생성될 수 있는 경우
2. 코드로 알아보는 데코레이터 패턴 필요한 상황
댓글을 달고 싶어요.
댓글에 공백이 없으면 좋겠어요.
광고성 댓글은 필터링 됐으면 좋겠어요.
공백도 없고 필터링도 됐으면 좋겠어요.
욕설을 제거됐으면 좋겠어요.
공백 + 광고 필터링 + 욕설 필터링이 됐으면 좋겠어요. ...
클라이언트는 결국 댓글(Comment)을 추가하는 행동을 하지만, 요구사항이 빈번하게 변경되는 상황이다.
2-1. 댓글을 달고 싶어요.
Client 코드
댓글을 다는 메소드를 호출한다. 변경되지 않았으면 하는 코드 이다.
ClientService 코드
댓글을 다는 기능(메소드)을 제공한다.
Main 코드
2-2. 댓글에 공백이 없었으면 좋겠어요.
CommentService를 확장해서 trim 기능을 추가로 제공해주는 별도의 서비스인 TrimmingCommentService를 만들어 사용한다.
TrimmingCommentService 코드
CommentService 상속받아서 확장
addComent()하기 전에 trim을 해주도록 기능 추가
Client에서 사용
new TrimmingCommentService()
구현체를 넣어줘서 사용한다.
기존 코드 변경없이 새로운 기능을 추가할 수 있다.
하지만 이 방식은 상속을 사용해서 컴파일 타임에 고정적으로 의존성이 정해진다. (유연하지 않다.)
2-3. 광고성 댓글은 필터링 됐으면 좋겠어요.
광고 필터링을 하는 CommentService를 또 만들 수 있다.
SpamFilteringCommentService 코드
comment에 http 문자가 들어가 있으면 스팸처리하여 출력하지 않는다.
new SpamFilteringCommentService()
구현체를 넣어줘서 사용한다.
2-4. 공백도 없고 필터링도 됐으면 좋겠어요.
두개의 기능을 모두 담은 새로운 Service 클래스를 생성해야 한다.
이 때 부터 단일 상속의 문제를 느끼게 된다.
단일 상속만 되기 때문에, 두 가지 기능을 제공하는 TrimAndSpamFilteringCommentService를 또 하나 만들게 된다.
클라이언트 코드는 바뀌지 않지만 상속만으로 확장해나가기 불편한 구조이다.
요구사항이 추가돼서 경우에 따라서 동적으로 필터링을 적용하거나 안하거나 해야한다면?
enabledSpamFilter
, enabledTrimmin
플래그 설정 값에 따라 동적으로 필터링서비스를 골라서 적용하고 싶다.
이 문제를 데코레이터 패턴으로 해결할 수 있다.
3. 데코레이터 패턴 적용
CommentService
Component (Interface)
DefaultCommentService
ConcreteComponent
기존에 CommentService에서 구체적으로 하던 작업을 여기에 구현
CommentDecorator
Decorator
SpamFilteringCommentService와 TrimmingCommentService를 추상화 시킨 데코레이터
SpamFilteringCommentDecorator, TrimmingCommentDecorator
ConcreteDecorator
SpamFilteringCommentService와 TrimmingCommentService에서 하던 작업을 여기에 구현
3-1. Component (Interface) 정의 - CommentService
3-2. ConcreteComponent 구현 - DefaultCommentService
기존에 CommentService에서 구체적으로 하던 작업을 여기에 구현
3-3. Decorator 구현
동일한
CommentService
타입이어야 한다.딱 하나의
wrappee
라고 하는 하나의 Component 타입의 인스턴스 타입을 가지고 있다.⇒
CommentService
하나를 가지고 있는 형태이다.그냥 가지고 있는 Component를 호출해주기만 하면 된다. (데코레이터의 역할 끝)
3-4. Trim 기능을 제공하는 Decorator 구현 - TrimmingCommentDecorator
CommentDecorator
를 구현addComment()
를 오버라이드 하는데, 이 때 부가적인 trim 기능을 추가한다.
3-5. Spam Filter 기능을 제공하는 Decorator 구현 - SpamFilteringCommentDecorator
CommentDecorator
를 구현addComment()
를 오버라이드 하는데, 이 때 부가적인 스팸 필터 기능을 추가한다.
CommentDecorator(Decorator
)에 들어올 수 있는 것은 DefaultCommentService(ConcreteComponent
)여도 되고, SpamFilteringCommentDecorator, TrimmingCommentDecorator인 ConcreteDecorator
여도 된다.
CommentService
타입이기만 하면 된다.
이런 유연성을 가지게하기 위해서 Decorator와 ConcreteComponent가 같은 인터페이스를 구현하도록 한 것이다.
3-6. Client
인터페이스 타입인 CommentService를 사용하면 된다.
3-7. Application 코드
Client가 사용할 구체적인 CommentService 구현체는 런타임시에 동적으로 바뀔 수 있게 된다.
SpamFilter도 사용하고 Trim도 사용한다고 하면, Decorator가 Decorator를 감싸는 구조가 된다.
즉, 두 기능 모두 수행하게 된다.
⇒ 또 다른 기능을 추가할 때 상속을 사용한다면 또 다른 상속 클래스를 만들어야 했지만, 이제는 Decorator가 Decorator를 감쌀 수 있는 구조이기 때문에 유연하게 사용할 수 있다.
main 코드가 늘어나는 것이 아닐까? yes. 이 부분의 코드는 객체를 동적으로 조합해서 전달해주는 부분이다.
스프링부트를 쓰고 있다면 자바 메소드를 통해 빈을 정의할 수 있다. 이 코드에 application.properties에 설정한 값에 따라 각기 다른 빈을 만들어서 전달해주도록 하면 된다.
4. 장단점
4-1. 장점
새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있다.
데코데리터는 자신이 해야하는 일만 하고, 이를 조합하는 것은 사용하는 측에서 정한다.
⇒
SRP(단일 책임 원칙)
객체지향 원칙을 따른다.Component 코드를 수정하지 않고, Client 코드도 수정하지 않으면서 기능을 확장할 수 있다.
⇒
OCP(개방 폐쇄 원칙)
객체지향 원칙을 따른다.
컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
Client가 구현체가 아닌 인터페이스를 사용한다.
⇒
DI(의존 역전 원칙)
객체지향 원칙을 따른다.
4-2. 단점
데코레이터를 조합하는 코드가 복잡할 수 있다.
데코레이터 패턴은 데코레이터가 스택과 같이 쌓이면서 행위를 추가한다는 점에서 객체의 데이터를 바꾸거나 내부 프로퍼티가 수정되는 행위를 한다면 데코레이터가 쌓이는 순서에 따라 결과물이 달라질 수 있을 것 같다. 이러한 이유로 데코레이터를 적용한다면 데코레이터를 감싸는 부분의 순서가 중요한 시스템에 대해서는 문서화와 공유를 잘하거나 이를 한번 더 감싼 객체를 추가해 순서를 강제하는 방법을 찾아야 할 것 같고 비동기 환경에는 적용하기 힘들어 보인다.
5. 실무 사용 예
자바
InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
java.util.Collections가 제공하는 메소드들 활용한 랩퍼
javax.servlet.http.HttpServletRequest/ResponseWrapper
스프링
ServerHttpRequestDecorator
5-1. InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
어댑터 패턴에서 다뤘던 예시와 같다.
어댑터 패턴의 목적 : 한 인터페이스를 다른 인터페이스로 변환한다.
패턴의 목적에 따라 어댑터 패턴이라 볼 수도 있고 데코레이터 패턴이라 볼 수도 있다.
하나의 타입을 다른 타입이 감싸는 구조이다.
InputStream → InputStreamReader → BufferedReader
이 과정에서 부가기능이 추가된다. 점점 더 로우 레벨을 고차원 수준의 API로 다룰 수 있도록 추가된다.
5-2. java.util.Collections가 제공하는 메소드들 활용한 랩퍼
checkedXXX()
: 기존 컬렉션 인스턴스를 부가적인 기능을 추가해서 다른 타입으로 변환해주는 메소드타입 체크를 제공해준다.
해당 컬렉션의 오퍼레이션을 실행할 때, 컬렉션에 들어가는 인스턴스들의 타입을 확인한다.
synchronizedXXX()
: 컬렉션에 여러 오퍼레이션이 들어올 때, 동시에 접근하지 못하도록 동기화 처리하는 메소드unmodifiableXXX()
: 컬렉션을 불변 객체로 취급하는 오퍼레이션wrapper을 이용해서 기능을 변경한 것이다.
5-3. javax.servlet.http.HttpServletRequest/ResponseWrapper
서블릿에서 제공해주는 Wrapper로 일종의 데코레이터 패턴이라고 볼 수 있다.
HttpServletRequest를 확장해서 HttpServletRequestWrapper가 제공하는 기능을 오버라이딩해서 부가적인 기능을 추가할 수 있다.
ex) HTTP 요청 메시지 본문을 다르게 처리해서 본다. 캐싱한다. 로깅을 남긴다. 의심스러운 요청 확인 등등의 작업을 해야할 때, 이런 wrapper를 만들어서 사용할 수 있다.
wrapper을 만들어서 HttpServletRequest를 담고, filter를 거치도록 하면, 항상 이 wrapper을 거쳐서 요청이 처리된다.
5-4. ServerHttpRequestDecorator
BeanDefinitionDecorator
빈 설정 데코레이터
직접사용할 일이 드물다. (스프링의 인프라쪽)
ServerHttpRequestDecorator / ServerHttpResponseDecorator
웹플럭스 HTTP 요청 /응답 데코레이터
Webflux를 쓰면 사용하게될 수도 있다.
ServerHttpRequest와 ServerHttpResponse를 커스터마이징할 수 있는 인터페이스이다.
이 데코레이터를 상속받는 클래스를 만들어서WebFilter를 거쳐가는 모든 요청이 이 데코레이터의 하위 클래스를 거쳐가게 된다.
리액티브의 WebFilter를 상속해서 만든 Filter를 거쳐가는 요청인 ServerHttpRequest를 (데코레이터에) 담아준다.
6. 다른 패턴들과 비교
어댑터 : 기능을 확장한다는 점에서 비슷하다고 할 수 있으나 어댑터는 기존 인터페이스와는 다른 인터페이스를 반환하고, 데코레이터는 인터페이스를 변경하지 않고 기능이 향상된 인터페이스를 제공한다.
컴포짓 : 합성을 이용한 패턴이라는 점에서는 비슷하나 컴포짓은 관련된 기능을 가지고 있는 객체들을 하나의 추상층으로 묶어 결과를 집계(요약) 한다면, 데코레이터는 해당 객체에 책임을 추가한다.
Last updated