Java/Design Pattern

[디자인 패턴] 구조 패턴 - 데코레이터 패턴 (Decorator Pattern)

민돌v 2023. 3. 7. 20:42
728x90
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 

 


✔️ 데코레이터 패턴

' 데코레이터 패턴은 기존코드를 변경하지 않고 부가기능을 추가하는 패턴이다. ' 라고 정의됩니다.
  • 데코레이터 패턴이란, 객체의 결합 을 통해 기능을 동적으로 유연하게 확장 할 수 있게 해주는 패턴입니다.
    • 동적으로 유연하게 확장할 수 있다는건→ 런타임 시에 부가기능을 추가하는 것도 가능하다는 이야기 입니다.
    • 객체의 결합이란 상속이 아닌, 위임(Delegation 혹은 합성이라고도 하는것 같음) 즉 Has-a 관계를 가지는 패턴임을 의미합니다.
    • 데코레이터 패턴은 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 패턴입니다.

 

데코레이터 패턴의 구조

 

→ 데코레이터 패턴의 구조를 살펴보면, Decorator가 기존의 기능을 정의한 Component 클래스를 감싸고 있습니다. 즉, 멤버변수로써 가지고있으면서, Component 가 가지는 기능(Operation)을 정의하고 있습니다.

→ 그럼 이제 기능의 확장은 Component 클래스를 직접 바라보지 않고 Decorator 를 상속받아 확장해 나갈 수 있습니다.

→ 풀어설명하자면, Component 의 기능을 구현한 Decorator 는 부모 인터페이스를 멤버변수로 가지고 있기에 외부에서 의존성을 주입받을 수 있는 상태가 되고

이 Decorator 의 기능을 확장한 자식 클래스(기능을 확장한 Decorator) 는 부모의 기능을 참조하여 사용하는 구조를 이루게되면,

기능을 확장한 Decorator 를 외부에서 주입받아 확장되는 형태가 가능해집니다!

 

말로는 복잡하니.. 코드로 보시져!


✔️ 데코레이터 패턴 예시

먼저 데코레이터 패턴을 적용하지 않고 상속으로 기능을 확장했을 때와 → 이걸 데코레이터 패턴으로 기능을 확장해나가는 예시로 변경해보겠슴당!

 

  • 예시는 간단하게, client 가 댓글을 작성하고, 작성한 댓글을 출력하는 예시입니다.
  • 댓글 출력 기능을 단순 출력 → 특정 문자 변경 (trim) → 특정 문자 탐지 (SpamFiltering) 으로 기능을 확장해나갈 시나리오입니다.

1. 상속을 통한 기능의 확장 예시

Client.class 와 CommentService

  • 단순하게 Client는 CommentService 의 addComment 기능을 통해 댓글을 작성할 수 있습니다.
public class Client {

    private CommentService commentService;

    public Client(CommentService commentService) {
        this.commentService = commentService;
    }

    public void writeComment(String comment) {
        commentService.addComment(comment);
    }
}

public class CommentService {

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

 

App.Class

  • 외부에서 Client 에 CommentService 를 주입하여 기능을 참조할 수 있도록 해줍니다.
  • 단순하게 댓글을 작성하면 그대로 출력함을 확인할 수 있습니다.
public class App {

    public static void main(String[] args) {
        Client client = new Client(new CommentService());
        client.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        client.writeComment("내가 왕이 될 상인가");
        client.writeComment("내가임마!!! 어이!!");
    }
}

댓글 결과

👏🏻 이제 기능을 확장해 봅니다. 가장 간단한 방법은 "상속" 을 통해 기능을 확장해 나가는 것입니다.

1) tirm 기능의 확장

TrimmingCommentService.class

/**
 *  상속으로 문제를 풀려고하니, 모든 경우의 수에 맞는 하위 클래스를 만들어주야하는 문제점이 있음
 */
public class TrimmingCommentService extends CommentService{

    @Override
    public void addComment(String comment) {
        super.addComment(trim(comment));
    }

    private String trim(String comment) {
        return comment.replace("!", "~");
    }
}

2) Spam 필터링 기능 확장

SpamFilteringCommentService.class

public class SpamFilteringCommentService extends CommentService{

    @Override
    public void addComment(String comment) {
        if (isNotSpam(comment)) {
            super.addComment(comment);
        }
    }

    private boolean isNotSpam(String comment) {
        return !comment.contains("http");
    }
}

 

이제 외부에서 호출할때, 상속받아 기능을 확장한 구현체를 상황에 맞게 주입해주면 기능을 확장에서 사용할 수 있습니다.

public class App {

    public static void main(String[] args) {
        Client client = new Client(new CommentService());
        client.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        client.writeComment("내가 왕이 될 상인가");
        client.writeComment("내가임마!!! 어이!!");

        System.out.println("--------- ");
        Client trimClient = new Client(new TrimmingCommentService());
        trimClient.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        trimClient.writeComment("내가 왕이 될 상인가");
        trimClient.writeComment("내가임마!!! 어이!!");


        System.out.println("--------- ");
        Client spamFilterClient = new Client(new SpamFilteringCommentService());
        spamFilterClient.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        spamFilterClient.writeComment("내가 왕이 될 상인가");
        spamFilterClient.writeComment("내가임마!!! 어이!!");
    }
}

📌 상속을 통한 기능의 확장 단점

  • 가장 단순한 방법이지만, 만약 trim 기능과 spam 기능을 동시에 하고싶다면, TrimSpamFilteringComment~~ 와 같은 추가적인 구현 클래스를 다시 만들어야하는 단점이 존재합니다.
  • 이러한 단점을 커버하기위해 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 데코레이터 패턴을 사용할 수 있습니다.

 

 


2. 위임을 이용한 데코레이터 패턴 설계를 통한 기능 확장 예시

Client.class 와 CommentService

  • 예제는 똑같이 client가 commentService 의 기능을 이용해 댓글을 작성합니다.
  • 하지만, CommentService를 인터페이스를 기능을 분리합니다. (ISP)
  • 그리고, 인터페이스의 가장 기본적인 기능을정의한 구현체 DefaultCommentService를 구현해줍니다.
public class Client {

    private CommentService commentService;

    public Client(CommentService commentService) {
        this.commentService = commentService;
    }

    public void writeComment(String comment) {

        commentService.addComment(comment);
    }
}

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

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

이렇게한다면 일단, 외부에서 클라이언트는 위와 동일하게 단순한 댓글 작성 기능을 사용할 수 있습니다.

public class App {

    public static void main(String[] args) {
        CommentService commentService = new DefaultCommentService();

        Client client = new Client(commentService);
        client.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        client.writeComment("내가 왕이 될 상인가");
        client.writeComment("내가임마!!! 어이!!");
    }
}

그럼 이제 데코레이터 패턴을 적용하여 기능을 확장해 봅시다!


1. 데코레이터 패턴의 핵심

  • Decorator 의 역할을 하는 클래스가 딱 1개의 Commponent 만을 가지고 있으면서, 기능을 확장해 나가는 것입니다. (has-a 관계)
public class CommentDecorator implements CommentService {

    private CommentService commentService;

    public CommentDecorator(CommentService commentService) {
        this.commentService = commentService;
    }

    @Override
    public void addComment(String comment) {
        commentService.addComment(comment);
    }
}

 

2. 기능의 확장

  • 그리고 난 후, 모든 기능의 확장은 Component 클래스를 감싸고있는 Decorator를 통해 이루어집니다.

1) trim 기능을 확장한 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("!", "~");
    }
}

2) SpamFilter 기능을 확장한 SpamFilteringDecorator

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

👏🏻 여기서 볼것은, 확장된 기능을 Overriding 한 각각 구현체에서 부모의 기능을 참조하여 기능을 확장한다는 것입니다.

그렇기 때문에 조합이 가능한 설계가 된 것이죠

 

3. APP

  • 데코레이터 패턴은 런타임 시에 기능을 확장할 수 있다고 했습니다.
  • 아래처럼 단순하게 조건문을 걸어 런타임 시 기능을 확장할 수 도 있습니다.
  • 지금 구조를 보시면
    1. Client 에 주입되는 CommentService 의 구현체는 → TrimmingCommentDecorator
    2. TrimmingCommentDecorator 에 주입되는 CommentService 의 구현체는 → SpamFilteringCommnetDecorator
    3. SpamFilteringCommnetDecorator 에 주입되는 CommentService 의 구현체는 → DefaultCommentService
  • 로, CommentService 인터페이스를 감싸고 있는 CommentDecorator의 구현체를 통해 기능을 이어나가고 있습니다. (DefaultCommentService 제외)
/**
 *  데코레이터패턴을 사용하여 상속이 아닌 위임으로 문제를 해결
 */
public class App {

    private static boolean enabledSpamFilter = true;
    private static boolean enableTrimming = true;

    public static void main(String[] args) {
        CommentService commentService = new DefaultCommentService();

        if (enabledSpamFilter) {
            commentService = new SpamFilteringCommentDecorator(commentService);
        }

        if (enableTrimming) {
            commentService = new TrimmingCommentDecorator(commentService);
        }

        Client client = new Client(commentService);
        client.writeComment("오징어 게입 수위 미쳤다ㄷㄷ → http://광고광고.com");
        client.writeComment("내가 왕이 될 상인가");
        client.writeComment("내가임마!!! 어이!!");

    }
}

 

이렇게 함으로써, 기능이 결합되어 새로운 구현체를 만들지 않고도 trim 기능과 spam filter 기능을 조합해 사용할 수 있는 구조가 된것입니다!

 

 


이상으로 데코레이터 패턴에 대해 공부해보았습니다

디자인 패턴을 공부할 수록 객체의 상속보다는 위임(or 합성이란 단어)을 많이 사용하는데

이에 대해 친숙해질 필요가 있다고 느껴지네요,,

 

그럼 끝!

 

 

반응형