본문 바로가기
Computer Sience/Desgin Pattern

[Design Pattern] 데코레이터 (Decorator) 패턴

by 제우제우 2024. 10. 28.

데코레이터 (Decorator) 패턴

기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴

상속이 아닌 위임을 사용해서 보다 유연하게(런타임에) 부가 기능을 추가하는 것도 가능하다.

데코레이터 (Decorator) 패턴 before

public class CommentService {
    public void addComment(String comment){
        System.out.println(comment);
    }
}

public class Client {
    private final CommentService commentService;
    public Client(CommentService commentService) {
        this.commentService = commentService;
    }
    public void writeComment(String comment){
        commentService.addComment(comment);
    }
    public static void main(String[] args) {
        Client client = new Client(new CommentService());
        client.writeComment("댓글1");
        client.writeComment("...댓글2...");
        client.writeComment("https://baejeu1212.tistory.com");
    }
}

 

CommentService 클래스

CommentService 클래스는 단순하게 addComment 메소드를 하나 가진다.

 

Client 

클라이언트 클래스이다. 

생성자로 의존객체인 CommentService를 주입 받는다. 

writeComment()를 호출하면 CommentService의  addComment() 메소드를 호출한다. 

 

출력 결과 

댓글1
...댓글2...
https://baejeu1212.tistory.com

 

여기에 추가로 CommentService의 기능을 확장하여 댓글에 ...이 있으면 없애서 댓글을 다는 기능을 만드려고한다. 

...댓글2... → 댓글2

 

CommentService를 상속하는 클래스 TrimmingCommentService 클래스를 생성하겠다. 

 

TrimmingCommentService

public class TrimmingCommentService extends CommentService{
    @Override
    public void addComment(String comment) {
        super.addComment(trim(comment));
    }
    private String trim(String comment){
        return comment.replaceAll("...", "");
    }
}

 

클라이언트 코드는 별도의 변경 없이 Client 생성자에 TrimmingCommentService를 주입해 주면 된다.

public static void main(String[] args) {
    Client client = new Client(new TrimmingCommentService());
    client.writeComment("댓글1");
    client.writeComment("...댓글2...");
    client.writeComment("https://baejeu1212.tistory.com");
}

 

출력 결과 

댓글1
댓글2
https://baejeu1212.tistory.com


이번에는 댓글 앞에 https 스키마가 붙으면 해당 댓글을 스팸처리하려고 한다. 

마찬가지로 CommentService를 상속받는 SpamFilteringCommentService를 만들어서 해결하겠다. 

 

SpamFilteringCommentService

public class SpamFilteringCommentService extends CommentService{
    @Override
    public void addComment(String comment) {
        if(!isSpam(comment)){
            super.addComment(comment);
        }
    }
    public boolean isSpam(String comment){
        if(comment.startsWith("https")){
            return true;
        }
        return false;
    }
}

comment가 https로 시작하면 출력하지 않는 코드이다. 

 

클라이언트 코드는 별도의 변경 없이 Client 생성자에 SpamFilteringCommentService를 주입해 주면 된다.

public static void main(String[] args) {
    Client client = new Client(new SpamFilteringCommentService());
    client.writeComment("댓글1");
    client.writeComment("...댓글2...");
    client.writeComment("https://baejeu1212.tistory.com");
}

 


출력 결과

댓글1
...댓글2...

 

이번에는 SpamFilteringCommentsService의 스팸처리 기능과 TrimmingCommetService의 trim 기능을 같이 동작하는 댓글 기능을 만들고 싶다. 

또한 클라이언트에 각 기능을 boolean 으로 설정하고 기능이 true면 해당 기능을 사용하는 방식으로 진행하고 싶다는 요구사항이 내려왔다.

 

문제는 클래스의 상속은 단일 상속만 가능하다. 

그렇다고 2기능을 모두 하는 CommentService를 상속받는 새로운 클래스를 만들어야 할까?

이럴 때 상속이 아닌 위임을 하는 방식으로 부가 기능을 추가하는 데코레이터 패턴을 적용하면 런타임에 유연하게 기능 추가가 가능하다. 

데코레이터 (Decorator) 패턴 after

Component - CommentService

public interface CommentService {
    void addComment(String comment);
}

 

ConcreteComponent & Decorator가 구현할 인터페이스이다. 

 

ConcreteComponent - DefaultCommentService

public class DefaultCommentService implements CommentService{
    @Override
    public void addComment(String comment) {
        System.out.println(comment);
    }
}

 

DefaultCommentService는 CommentService를 구현하는 기본 CommentService이다. 

기본적인 댓글 출력 기능을 가진다. 

 

Decorator - CommentDecorator

public class CommentDecorator implements CommentService{
    private final CommentService commentService;
    public CommentDecorator(CommentService commentService) {
        this.commentService = commentService;
    }
    @Override
    public void addComment(String comment) {
        commentService.addComment(comment);
    }
}

 

Decorator 역할을 하는 CommentDecorator 이다. 

CommentService의 부가 기능은 해당 Decorator를 상속 받으면 된다. 

 

ConcreteDecorator1 - SpamFilteringCommentDecorator

public class SpamFilteringCommentDecorator extends CommentDecorator{
    public SpamFilteringCommentDecorator(CommentService commentService) {
        super(commentService);
    }
    @Override
    public void addComment(String comment) {
        if(!isSpam(comment)){
            super.addComment(comment);
        }
    }
    public boolean isSpam(String comment){
        if(comment.startsWith("https")) return true;
        return false;
    }
}

 

CommentDecorator를 상속 받은 ConcreteDecorator 이다. 

SpamFilteringCommentDecorator는 comment가 https로 시작하면 spam으로 판단하고 

생성자로 받는 CommentService의 addComment를 호출하지 않는다. 

 

중요!

생성자로 받는 CommentService 타입의 객체는 CommentDecorator의 구현체로 또 다른 부가 기능을 하는 구현체일 수도 있고 기본 기능을 하는 ConcreteComponent일 수도 있다. 

이렇게 구조를 설계를 하면 런타임에 유연하게 객체 생성이 가능하다. 

 


ConcreteDecorator2 - 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("...", "");
    }
}

 

CommentDecorator를 상속 받은 ConcreteDecorator 이다. 

TrimmingCommentDecorator는 댓글에 ... 이 있으면 없애주는 (부가) 기능을 한다. 

 

Client

public class Client {
    private final CommentService commentService;
    public Client(CommentService commentService) {
        this.commentService = commentService;
    }
    public void writeComment(String comment){
        commentService.addComment(comment);
    }
}

Client는 생성자 주입으로 CommentService를 받는다. 

writeComment()는 단순히 commentService의 addComment()를 호출한다. 

 

App

public class App {
    private static boolean enabledSpamFilter = true;
    private static boolean enabledTrimming = true;
    public static void main(String[] args) {
        CommentService commentService = new DefaultCommentService();
        if(enabledSpamFilter){
            commentService = new SpamFilteringCommentDecorator(commentService);
        }
        if(enabledTrimming){
            commentService = new TrimmingCommentDecorator(commentService);
        }
        Client client = new Client(commentService);
        client.writeComment("댓글1");
        client.writeComment("...댓글2...");
        client.writeComment("https://baejeu1212.tistory.com");
    }
}

 

Client에서 사용할 CommentService를 enabledSpamFilter와 enabledTrimming에 따라서 유연하게 생성한다. 

 

2가지 전부 true 

 

먼저 기본 기능을 하는 DefaultCommentService 객체를 생성한다. 

해당 Spam 데코레이터를 생성하면서 DefaultCommentService 객체를 넘겨준다. 

Trimming 데코레이터를 생성하면서 DefaultCommentService를 감싼 SpamDecorator를 넘겨준다. 

 

이렇게 데코레이터 패턴을 적용해서 2가지 기능을 모두 하는 새로운 CommtService를 상속받는 구현체를 만드는 게 아닌 

데코레이터 추가하고 의존 객체 주입을 통한 조립만 하면 2가지 기능을 하는 CommentService 객체를 만들 수 있다. 

이 패턴의 장점은 런타임에 enabledSpamFiter / enabledTrimming 같은 특정 case에 따라서 객체 생성을 유연하게 조립할 수 있다는 장점이 있다. 

데코레이터 (Decorator) 패턴 정리

기존 코드를 변경하지 않고 부가 기능을 추가하는 패턴

 

장점

새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있다

컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다

 

단점

데코레이터를 조합하는 코드가 복잡할 수 있다

 

데코레이터를 사용하지 않고 상속을 선택한다면 더 복잡하다.

그래서 단점으로 보기 애매하다. 

 

특징

데코레이터는 부가 기능만 기본 ConcreteComponent는 기본 기능만

→ SRP

public class CommentDecorator implements CommentService{
    private final CommentService commentService;
    public CommentDecorator(CommentService commentService) {
        this.commentService = commentService;
    }
}

의존 관계을 추상화(CommentService) 유연성과 재사용성을 높였다. 

→ DIP 

 

클라이언트는 CommetService 하나만을 의존한다. 해당 CommetService가 어떤 구현체들을 조립해서 만든지는 모른다. 

즉 기능이 바뀌어도 클라이언트 코드는 바뀌지 않는다.

→ OCP

 

데코레이터 패턴은 SOLID 원칙의 대부분의 원칙을 지키게 해주는 중요하고 대표적인 디자인 패턴이다.

 

데코레이터 (Decorator) 패턴 적용 사례

InputStream & Reader

try(InputStream is = new FileInputStream("input.txt");
    InputStreamReader isr = new InputStreamReader(is);
    BufferedReader reader = new BufferedReader(isr)) {
    while(reader.ready()) {
        System.out.println(reader.readLine());
    }
} 
catch (IOException e) {
    throw new RuntimeException(e);
}

보는 시각에 따라 해당 사례는 어댑터 패턴으로 볼 수도 있고 데코레이터 패턴으로 볼 수도 있다. 

BufferedReader는 Reader를 데코레이트하여 버퍼링 기능을 추가함으로써 읽기 기능을 향상한다. 

InputStreamReader는 InputStream을 Reader로 변환해주는 어댑터 역할을 한다.

 UnmodifiableList

public static void main(String[] args) {
    ArrayList<Integer> list = new ArrayList();
    list.add(1);
    list.add(2);

    List<Integer> unmodifiableList = Collections.unmodifiableList(list);
    unmodifiableList.add(3);
}

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1067)
	at com.example.desginpatternstudy._02_structural_patterns._09_decorator._03_java.DecoratorInJava.main(DecoratorInJava.java:14)

 

Collections.unmodifableList()는 리스트를 불변 리스트로 만들어준다.

불변 리스트에 원소를 추가하려고 하면 UnsupportedOperationException이 발생한다.

이 UnmodifiableList는 List 인터페이스에 불변성이라는 부가 기능을 가진 리스트이다. 

 

즉 List 인터페이스는 Component 이고 

다양한 구현체들 ArrayList, LinkedList는 ConcreteComponent

UnmodifiableList는 Decorator 

 

이 메소드는 List의 add()나 addAll() 메소드를 구현하고 있을 것이다. 

아마 원소를 더하는 게 아닌  UnsupportedOperationException을 던질 것이다. 

 

Collections 내부 클래스로 정의된 UnmodifiableList

역시나 읽기 api인 get()을 제외하고 모두 UnsupportedOperationException을 던진다. 

 

HttpServletRequestWrapper & HttpServletResponseWrapper 

public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
}

HttpServletRequest의 구현체 HttpServletRequestWrapper 이다.

 

필터에서 HttpServletRequest를 넘겨줄 때 해당 클래스를 상속 받는 구현체를 정의해서 

본인이 특정 메소드를 (부가 기능)재정의해서 객체를 넘겨줄 수 있다.

HttpServletRequestWrapper는 HttpServletRequest라는 Component의 Decorator인 것이다.

 

참고자료

백기선님 디자인 패턴 강의

 

코딩으로 학습하는 GoF의 디자인 패턴 강의 | 백기선 - 인프런

백기선 | 디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를

www.inflearn.com