개발자로 후회없는 삶 살기

디자인 패턴 PART.데코레이터 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.데코레이터 패턴

몽이장쥰 2023. 8. 20. 17:09

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

 

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

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

www.inflearn.com

 

본론

- 데코레이터 패턴 소개

기존에 코드를 변경하지 않으면서 부가적인 코드를 추가할 수 있는 패턴입니다. 부가 기능을 다이나믹하게(유연하게) 런타임에 동적으로 추가할 수 있습니다.

 

- 코드

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 CommentService());
        client.writeComment("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }
}

클라이언트 클래스에서 코멘트 서비스를 사용하고 있습니다.

 

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

클라이언트는 writeComment로 댓글을 씁니다.

 

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

코멘트를 남길 때 trimming으로 ... 같은 걸 없애고 싶습니다. 그러면 가장 쉬운 방법이 comment 서비스를 상속해서 super에 있는 addComment를 호출하기 전에 즉, 코멘트를 남기기전에 trim을 하면 됩니다.

 

이제 클라이언트에게 트리밍 서비스를 넣으면 됩니다. 그러면 출력을 해보면 ...이 트리밍되고 기존 코드는 수정되지 않습니다. 하지만 이렇게 클라이언트의 writeComment 코드가 정해진 상태에서 상속을 쓰면 유연하진 않습니다.

 

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");
    }
}

예를들어 여기서 광고를 제거를 해야해서 http 코멘트는 받을 수 없다고 해야하면 Spam 서비스를 또 만듭니다. 그러면 여기서 클라이언트의 생성자로 트리밍 대신 스팸 서비스를 넣어야 하는데

 

public static void main(String[] args) {
        Client client = new Client(new SpamFilteringCommentService());
        client.writeComment("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }

이러면 트리밍이 또 안되어서 트리밍과 스팸을 둘 다 하는 서비스를 또 만들어야 해서 이때부터 상속이 이런 문제가 있음을 느끼게 됩니다. 상속은 단일 상속만 되고 하나로 만들 수 없습니다. 그래서 이 경우에는 SpamAndTrim 서비스를 또 만들어야 합니다. 상속만으로는 계속 확장해 나가기가 쉽지 않습니다. 

또한 동적으로도 불가능 합니다. 만약 enabled로 Flag가 있어서 동적으로 어떠한 경우에는 trim을 하고 어떠한 경우에는 spam처리를 하는 것을 enabledSpam만 T면 Spam 서비스만 써야하고 둘 다 T면 And 서비스를 써야 하는데 이때부터 코드가 굉장히 지저분해지고 경우의 수에 맞게 다 클래스를 상속으로 확장해야 합니다.

이게 데코레이터 패턴을 적용하지 않았을 때 상속으로 문제를 해결할 때 생기는 문제입니다.

 

=> 구조

최상위에 컴포넌트 인터페이스가 있습니다. 얘는 Concrete와 데코레이터가 둘 다 구현합니다. 여기까지 보면 마치 컴포짓과 같아 보이지만,

 

public class Bag implements Component{

public class Item implements Component{

차이는 컴포짓에서는 컴포짓이 Item과 Bag등 여러개의 클래스를 가지고 있었습니다. 데코레이터는 그게 아니라 딱 하나의 데코레이터가 감싸고 있는(래피) 하나의 인스턴스를 가지고 있습니다. 그래서 데코레이터는 자기가 감싸고 있는 하나의 컴포넌트를 호출하는데 호출하기 전이나 뒤에 부가적인 일을 할 수 있습니다.

 

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

그래서 우리는 이제 CommentService 자체를 일종의 인터로 만들 것이고 CommentService에서 하던 구체적인 일은 ConcreteComponent로 옮겨 갈 것이고 이 트림 서비스와 스팸 서비스 둘을 추상화시킨 데코레이터를 만들고 그 안에서 컴포넌트를 참조하면서 (CommentService가 넣어질 것이고) 스팸 필터링과 트리밍 작업은 Concrete 데코레이터에서 하게 될 것입니다.

 

이전의 컴포짓 패턴처럼 여러개를 감싸지 않고 딱 하나만 감싸는 데코레이터가 있고 이 데코레이터를 상속받아서 각각의 Concrete로 역할을 정의할 것입니다. 그리고 구체 데코레이터를 조합해서 코드를 만들면 동적으로 변경하며 쓸 수 있습니다.

 

- 패턴 적용

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

컴포짓처럼 상위에 인터페이스부터 정의를 해야하고 CommentService로 인터페이스를 하며 addComment를 추상 메서드로 가집니다.

 

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

그리고 위에서 CommentService에서 하던 구체적인 일은 ConcreteComponent로 옮겨 갈 것이라고 하였으니 이전 CommentService에서 하던 일을 Default에서 Commnet 서비스를 구현해서 하도록합니다. 이렇게 하면 Comment Service(컴포넌트 인터페이스)와 Default(콘크리트 컴포넌트)가 정의되었습니다.

 

-> 데코레이터

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

이제 필요한 게 데코레이터로 뭔가를 감싸서 감싸고 있는 컴포넌트를 그냥 그대로 호출하기만 하면 됩니다.(랩퍼) 그것으로 Comment 데코레이터를 만들고 CommentService를 가지고 있습니다. 그리고 그 가지고 있는 것을 호출하기만 하면 되고 이게 데코레이터 역할의 전부입니다.

 

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

위에서 트림 서비스와 스팸 서비스 둘을 추상화시킨 데코레이터를 만들고 그 안에서 컴포넌트를 참조하면서 (CommentService가 넣어질 것이고) 스팸 필터링과 트리밍 작업은 Concrete 데코레이터에서 하게 될 것이라고 했습니다.

 

이제 데코레이터를 상속받아서 스팸 필터링 데코레이터, 트리밍 데코레이터를 만듭니다. 트리밍 데코레이터를 만들 때는 방금 만든 데코레이터를 상속하고 재정의를 합니다. 그리고 부모의 메서드를 호출하기 전에 부가적인 작업을 하는데 여기서는 trim이었습니다.

 

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");
    }
}

스팸도 데코레이터를 만듭니다. 여기서 신기한 건 데코레이터의 생성자로 서비스 타입을 넣어주는데 여기에 들어오는 게 스팸 데코레이터일 수도 있고 트림일 수도 있습니다.

 

public class DefaultCommentService implements CommentService{

public class CommentDecorator implements CommentService{

데코레이터들이 CommentDecorator를 상속하고 CommentDecorator가 CommentService를 상속하기 때문에 구체 데코레이터들이 CommentService 타입입니다.

 

-> 클라이언트

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

클라이언트 코드는 서비스를 사용하고 댓글을 달면(write) 끝입니다. 

 

-> 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("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }
}

어플에서는 클라이언트가 사용할 서비스 객체는 동적으로 바뀔 수 있습니다. 기본은 DefaultCommentService를 사용해서 댓글을 다는 것만 합니다. 근데 만약 런타임 시에 enable 스팸이 T라면 데코레이터를 적용해서 SpamFilteringCommentDecorator으로 감싸게 됩니다.

 

 

DefaultCommentService도 CommentService 타입이고 CommentDecorator도 CommentService 타입이라서 처음에 기본이었던 commentService를 받아서 부가처리를 할 수 있습니다.

 

-> 동적으로 처리

결과를 보면 enable Flag에 따라서 다른 처리 결과를 보입니다. 상속을 통해서 문제를 해결하려고 했을 때는 계속 또 다른 상속 클래스를 만들어야 했는데 이제는 데코레이터가 데코레이터를 감쌀 수 있기 때문에 만들 필요가 없습니다.

즉, 둘 다 True인 경우 And 구현체를 만들지 않아도 데코레이터가 데코레이터를 감싸면서 문제를 해결할 수 있습니다. 하지만 if 문은 늘어나는 것은 맞습니다. 이를 스프링을 쓴다면 빈을 정의할 때 application properties에 불러온 값에 따라서 빈을 등록할 수 있으니 이 코드는 사라지게 됩니다.

 

- 장, 단점
1. 한가지 역할

새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있습니다. 상속을 쓴다면 2개의 기능을 하는 새로운 상속 구현 클래스를 만들어야 했는데 이제는 본연의 1가지 일만 하는 데코레이터를 만들고 조합은 다른 곳에서 합니다. 이는 단일 책임 원칙을 만족하는 것입니다.

2. 다이나믹 처리

그리고 컴파일 타임이 아닌 런타임에 동작을 결정할 수 있습니다. 현재도 if문으로 조합을 복잡하게 하지만 데코레이터를 하지 않았고 상속만 사용했다면 정적으로 조합에 해당하는 클래스를 만들어야 합니다. 데코레이터에서는 조합이 동적으로 일어납니다. 이렇게 데코레이터가 SOLID에 해당하는 원칙을 많이 만족하는 좋은 패턴입니다.

3. if문으로 조합을 하는 것

조합을 하는 것과 클래스가 늘어나는 것이 단점으로 보일 수 있는데 사실 상속을 사용했을 때보다 적게 늘어난 것이고 데코레이터(CommentDecorator)와 CommentService(Component)만 들어난 것이라서 O(1)만큼 늘어난 것이라 문제는 아닙니다. 상속을 사용하면 경우의 수에 따라 클래스가 늘어나니 2^n으로 늘어날 것입니다.

Comments