데코레이터 패턴(Decorator Pattern)
기존 코드를 변경하지 않고 부가기능을 추가하는 패턴
- 상속이 아닌 위임을 사용해서 보다 유연하게 (런타임에) 부가 기능을 추가하는 것도 가능하다.
데코레이터 패턴 적용 전
웹 사이트에서 사용자들이 작성한 Comment 를 관리하는 어플리케이션이 있다 가정해보자.
Comment 를 관리하는 CommentService 는 아래와 같으며 addComment 는 Comment 를 출력하는 메서드이다.
public class CommentService {
public void addComment(String comment) {
System.out.println(comment);
}
}
만약, "..." 과 특수기호는 관리하고 싶지 않다면 그에 맞는 기능을 추가해야한다.
해당 기능은 아래와같이 TrimmingCommentService 로 구현할 수 있다.
public class TrimmingCommentService extends CommentService {
@Override
public void addComment(String comment) {
super.addComment(comment);
}
private String trim(String comment) {
return comment.replace("...", "");
}
}
이를 사용하는 Client 는 아래와 같이 구현할 수 있다.
public class Client {
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
private void writeComment(String comment) {
commentService.addComment(comment);
}
public static void main(String[] args) {
Client client = new Client(new TrimmingCommentService());
client.writeComment("오징어 게임");
client.writeComment("보는게 하는거보다 재밌을 수 가 없지...");
client.writeComment("https://hyokeun0419@tistory.com");
}
}
//결과
오징어 게임
보는게 하는거보다 재밌을 수 가 없지...
https://hyokeun0419@tistory.com
새로운 기능 추가
여기서 특수기호 관리가 아닌 광고를 걸러내기 위한 새로운 서비스를 구현해야 한다면 SpamFilteringCommentService 를 하나 만들고 이전과 같이 CommentService 를 상속하여 만든다.
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");
}
}
이를 사용하기 위해서는 Client 는 아래와 같이 의존관계만 바꿔주면 된다.
public class Client {
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
private void writeComment(String comment) {
commentService.addComment(comment);
}
public static void main(String[] args) {
Client client = new Client(new SpamFilteringCommentService());
client.writeComment("오징어 게임");
client.writeComment("보는게 하는거보다 재밌을 수 가 없지...");
client.writeComment("https://hyokeun0419@tistory.com");
}
}
//결과
오징어 게임
보는게 하는거보다 재밌을 수 가 없지...
실행을 해보면 trim("...") 은 적용되지 않았지만, http 가 포함된 comment 는 걸러진것을 확인할 수 있다.
문제발생
- SpamService , TrimService 모두 사용하려면 이 두 코드가 포함된 새로운 CommentService 를 만들어야 한다.
- 아니면, 새로운 CommentService 를 만들어 이 둘을 이중 상속받아야 한다.
이를 데코레이터 패턴을 적용하여 해결해보자.
데코레이터 패턴 적용 후
먼저 공통된 기본 기능을 가진 CommentService 인터페이스
그리고 그 기본 기능을 구현하는 DefaultCommentService 는 아래와 같다.
public interface CommentService {
void addComment(String comment);
}
// Concrete Component
public class DefaultCommentService implements CommentService {
@Override
public void addComment(String comment) {
System.out.println(comment);
}
}
다음은 Decorator 에 속하는 CommentDecorator 를 구현해준다.
// wrapper, decorator
public class CommentDecorator implements CommentService {
private CommentService commentService;
public CommentDecorator(CommentService commentService) {
this.commentService = commentService;
}
@Override
public void addComment(String comment) {
System.out.println(comment);
}
}
CommentDecorator 는 CommentService 를 참조로 가지는 것을 볼 수 있다.
이것을 wrappee 라고 하는데, 다양한 Decorator 를 참조하기 위해 반드시 필요하다.
ConcreteDecorator 들을 구현하자.
TrimmingCommentDecorator
public class TrimmingCommentDecorator extends CommentDecorator {
public TrimmingCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
super.addComment(trim(comment));
}
private String trim(String comment) {
return comment.replace("...", "");
}
}
SpamFilteringCommentDecorator
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");
}
}
DateCommentDecorator
import java.time.LocalDateTime;
public class DateCommentDecorator extends CommentDecorator{
public DateCommentDecorator(CommentService commentService) {
super(commentService);
}
@Override
public void addComment(String comment) {
super.addComment(addDate(comment));
}
private String addDate(String comment) {
return comment + " [" + LocalDateTime.now()+"]";
}
}
Client
public class Client {
//Component 사용
private CommentService commentService;
public Client(CommentService commentService) {
this.commentService = commentService;
}
public void writeComment(String comment) {
commentService.addComment(comment);
}
}
App
public class App {
private static boolean enableSpamFilter = true;
private static boolean enableTrimming = true;
private static boolean enableDate = true;
public static void main(String[] args) {
CommentService commentService = new DefaultCommentService();
//런타임시에 추가된다.
if (enableSpamFilter) {
commentService = new SpamFilteringCommentDecorator(commentService);
}
if (enableTrimming) {
commentService = new TrimmingCommentDecorator(commentService);
}
if (enableDate) {
commentService = new DateCommentDecorator(commentService);
}
Client client = new Client(commentService);
client.writeComment("오징어 게임");
client.writeComment("보는게 하는거보다 재밌을 수 가 없지...");
client.writeComment("https://hyokeun0419@tistory.com");
}
}
//결과
오징어 게임 [2022-07-19T18:12:51.824201]
보는게 하는거보다 재밌을 수 가 없지... [2022-07-19T18:12:51.839300]
https://hyokeun0419@tistory.com [2022-07-19T18:12:51.839345]
결과값과 아래 이미지와 같이 객체의 결합을 통해 모든 Decorator 가 적용된다는 것을 알 수 있다.
장점과 단점
장점
- 새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있다.
- 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
- 현재 Client가 인터페이스를 사용함으로써 DI원칙을 적용할 수 있다.
단점
데코레이터를 조합하는 코드가 복잡할 수 있다.
참고
'디자인 패턴' 카테고리의 다른 글
플라이웨이트 패턴 (Flyweight Pattern) (0) | 2022.07.22 |
---|---|
퍼사드 패턴 (Facade Pattern) (0) | 2022.07.19 |
컴포짓 패턴 (Composite Pattern) (0) | 2022.07.12 |
브릿지 패턴 (Bridge Pattern) (0) | 2022.07.12 |
어댑터 패턴 (Adapter Pattern) (0) | 2022.07.05 |