데코레이터 패턴
GoF의 디자인패턴 중 구조의 데코레이터 패턴을 Java 로 정리한 글
기존 코드를 변경하지 않고 부가 기능을 동적으로(유연하게) 추가하는 패턴
상속이 아닌 위임을 사용해서 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하다. 기능 확장이 필요할 때 상속대신 사용할 수 있는 패턴.

1
- Component (Interface)
- ConcreteComponent와 Decorator가 이를 구현하고 있다. 같은 오퍼레이션을 가지고 있다.
- Decorator
- 컴포짓 패턴의 Composite과 비슷해보이지만 차이점이 있다.
- Decorator는 단 하나의
wrappee
라고 하는 Component 타입의 인스턴스 타입을 가지고 있다. - 자신이 감싸고 있는 하나의 Component를 (오퍼레이션 내부에서) 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.
- 상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
- 객체의 결합을 통해 기능이 생성될 수 있는 경우
1.댓글을 달고 싶어요. 2.댓글에 공백이 없으면 좋겠어요. 3.광고성 댓글은 필터링 됐으면 좋겠어요. 4.공백도 없고 필터링도 됐으면 좋겠어요. 5.욕설을 제거됐으면 좋겠어요. 6.공백 + 광고 필터링 + 욕설 필터링이 됐으면 좋겠어요. ...
클라이언트는 결국 댓글(Comment)을 추가하는 행동을 하지만, 요구사항이 빈번하게 변경되는 상황이다.
Client 코드
- 댓글을 다는 메소드를 호출한다. 변경되지 않았으면 하는 코드 이다.
public class Client {
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
private void writeComment(String comment) {
commentService.addComment(comment);
}
}
ClientService 코드
- 댓글을 다는 기능(메소드)을 제공한다.
public class CommentService {
public void addComment(String comment) {
System.out.println(comment);
}
}
Main 코드
public class CommentApp {
Client client = new Client(new CommentService());
client.writeComment("디자인패턴 스터디");
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
client.writeComment("https://github.com/jun108059");
}
CommentService를 확장해서 trim 기능을 추가로 제공해주는 별도의 서비스인 TrimmingCommentService를 만들어 사용한다.
TrimmingCommentService 코드
- CommentService 상속받아서 확장
- addComent()하기 전에 trim을 해주도록 기능 추가
public class TrimmingCommentService extends CommentService {
@Override
public void addComment(String comment) {
super.addComment(trim(comment));
}
private String trim(String comment) {
return comment.replace("...", "");
}
}
- Client에서 사용
new TrimmingCommentService()
구현체를 넣어줘서 사용한다.
public class CommentApp {
Client client = new Client(new TrimmingCommentService());
client.writeComment("디자인패턴 스터디");
client.writeComment("보는게 하는거 보다 재밌을 수가 없지..."); // 공백 제거
client.writeComment("https://github.com/jun108059");
}
기존 코드 변경없이 새로운 기능을 추가할 수 있다.
하지만 이 방식은 상속을 사용해서 컴파일 타임에 고정적으로 의존성이 정해진다. (유연하지 않다.)
광고 필터링을 하는 CommentService를 또 만들 수 있다.
SpamFilteringCommentService 코드
- comment에 http 문자가 들어가 있으면 스팸처리하여 출력하지 않는다.
public class SpamFilteringCommentService extends CommentService {
@Override
public void addComment(String comment) {
boolean isSpam = isSpam(comment);
if (!isSpam) {
super.addComment(comment);
}
}
private boolean isSpam(String comment) {
return comment.contains("http");
}
}
new SpamFilteringCommentService()
구현체를 넣어줘서 사용한다.
public class CommentApp {
Client client = new Client(new CommentService());
client.writeComment("디자인패턴 스터디");
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
client.writeComment("https://github.com/jun108059");
}
두개의 기능을 모두 담은 새로운 Service 클래스를 생성해야 한다.
이 때 부터 단일 상속의 문제를 느끼게 된다.
단일 상속만 되기 때문에, 두 가지 기능을 제공하는 TrimAndSpamFilteringCommentService를 또 하나 만들게 된다.
클라이언트 코드는 바뀌지 않지만 상속만으로 확장해나가기 불편한 구조이다.
요구사항이 추가돼서 경우에 따라서 동적으로 필터링을 적용하거나 안하거나 해야한다면?
enabledSpamFilter
, enabledTrimmin
플래그 설정 값에 따라 동적으로 필터링서비스를 골라서 적용하고 싶다.public class Client {
private CommentService commentService;
private boolean enabledSpamFilter;
private boolean enabledTrimmin;
//..생략
}
이 문제를 데코레이터 패턴으로 해결할 수 있다.
- CommentService
- Component (Interface)
- DefaultCommentService
- ConcreteComponent
- 기존에 CommentService에서 구체적으로 하던 작업을 여기에 구현
- CommentDecorator
- Decorator
- SpamFilteringCommentService와 TrimmingCommentService를 추상화 시킨 데코레이터
- SpamFilteringCommentDecorator, TrimmingCommentDecorator
- ConcreteDecorator
- SpamFilteringCommentService와 TrimmingCommentService에서 하던 작업을 여기에 구현

2
public interface CommentService {
void addComment(String comment);
}
- 기존에 CommentService에서 구체적으로 하던 작업을 여기에 구현
public class DefaultCommentService implements CommentService {
@Override
public void addComment(String comment) {
System.out.println(comment);
}
}
- 동일한
CommentService
타입이어야 한다. - 딱 하나의
wrappee
라고 하는 하나의 Component 타입의 인스턴스 타입을 가지고 있다. - ⇒
CommentService
하나를 가지고 있는 형태이다. - 그냥 가지고 있는 Component를 호출해주기만 하면 된다. (데코레이터의 역할 끝)
public class CommentDecorator implements CommentService {
private CommentService commentService;
public CommentDecorator(CommentService commentService) {
this.commentService = commentService;
}
@Override
public void addComment(String comment) {
commentService.addComment(comment);
}
}
CommentDecorator
를 구현addComment()
를 오버라이드 하는데, 이 때 부가적인 trim 기능을 추가한다.
public class TrimmingCommentDecorator extends CommentDecorator {
public TrimmingCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
super.addComment(trim(comment)); //trim 기능 추가
}
private String trim(String comment) {
return comment.replace("...", "");
}
}
CommentDecorator
를 구현addComment()
를 오버라이드 하는데, 이 때 부가적인 스팸 필터 기능을 추가한다.
public class SpamFilteringCommentDecorator extends CommentDecorator {
public SpamFilteringCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
if (isNotSpam(comment)) { // 스팸 필터 기능 추가
super.addComment(comment);
}
}
private boolean isNotSpam(String comment) {
return !comment.contains("http");
}
}
CommentDecorator(
Decorator
)에 들어올 수 있는 것은 DefaultCommentService(ConcreteComponent
)여도 되고, SpamFilteringCommentDecorator, TrimmingCommentDecorator인 ConcreteDecorator
여도 된다.CommentService
타입이기만 하면 된다.이런 유연성을 가지게하기 위해서 Decorator와 ConcreteComponent가 같은 인터페이 스를 구현하도록 한 것이다.
- 인터페이스 타입인 CommentService를 사용하면 된다.
public class Client {
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
public void writeComment(String comment) {
commentService.addComment(comment);
}
}
- Client가 사용할 구체적인 CommentService 구현체는 런타임시에 동적으로 바뀔 수 있게 된다.
- SpamFilter도 사용하고 Trim도 사용한다고 하면, Decorator가 Decorator를 감싸는 구조가 된다.
- 즉, 두 기능 모두 수행하게 된다.
public class App {
private static boolean enabledSpamFilter = true;
private static boolean enabledTrimming = true;
public static void main(String[] args) {
CommentService commentService = new DefaultCommentService();
// 런타임시에 플래그 값에 따라 commentService가 결정된다.
if (enabledSpamFilter) {
commentService = new SpamFilteringCommentDecorator(commentService);
}
if (enabledTrimming) {
commentService = new TrimmingCommentDecorator(commentService);
}
Client client = new Client(commentService);
client.writeComment("디자인패턴 스터디");
client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
client.writeComment("https://github.com/jun108059");
}
}
⇒ 또 다른 기능을 추가할 때 상속을 사용한다면 또 다른 상속 클래스를 만들어야 했지만, 이제는 Decorator가 Decorator를 감쌀 수 있는 구조이기 때문에 유연하게 사용할 수 있다.
main 코드가 늘어나는 것이 아닐까? yes. 이 부분의 코드는 객체를 동적으로 조합해서 전달해주는 부분이다.
스프링부트를 쓰고 있다면 자바 메소드를 통해 빈을 정의할 수 있다. 이 코드에 application.properties에 설정한 값에 따라 각기 다른 빈을 만들어서 전달해주도록 하면 된다.