데코레이터 (Decorator) 패턴

weekbelt·2022년 12월 12일
0

1. 패턴 소개

데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가합니다. 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 런타임 환경에서 유연하게 확장할 수 있는 방법을 제공합니다.

위의 패턴 구조를 보면 컴포짓패턴처럼 최상위에 Component역할을 하는 인터페이스가 있습니다. 이 인터페이스는 ConcreteComponent와 Decorator가 둘다 구현을 한 인터페이스이고 operation메서드를 가지고 있습니다. 컴포짓패턴과 굉장히 유사합니다. 하지만 여기서의 차이점은 Decorator가 컴포짓패턴에 있던 Composite타입처럼 여러개의 Decorator를 가지고 있는게 아니라 딱 하나의 Component타입의 인스턴스를 가지고 있습니다. Decorator는 자기가 감싸고 있는 하나의 컴포넌트를 호출하는데 호출하기 전이나 뒤에 부가적인 일들을 할 수 있습니다.

데코레이터 패턴을 적용하기 이전의 예시 코드를 살펴보겠습니다. 댓글을 남기는 기능이 있는 CommentService를 생성자로 주입받아서 writeComment메서드에서 comment를 쓰고 있습니다.

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

실행 결과

오징어게임
보는게 하는거 보다 재밌을 수가 없지...
http://whiteship.me

우리가 comment를 남길때 필요없는 문자들을 정리하거나 trimming을 하고싶을 수 있습니다. 이런경우 가장 먼저 떠오르는 방법이 CommentService를 확장해서 trimming기능이 추가된 서비스를 생성하는 방법입니다.

CommentService를 확장해서 trimming하는 기능을 추가한 클래스

public class TrimmingCommentService extends CommentService {

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

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

}

CommentService를 상속한 다음 comment메서드를 호출하기전에 trim메서드를 통해서 필요없는 문자들을 제거하는 과정을 거칩니다. 이렇게 정의한 클래스를 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()); // Trimming이 추가된 서비스를 주입
        client.writeComment("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }

}

Comment클래스를 확장하기 전의 결과와 다르게 '...'이런 문자열이 제거되었습니다.

실행 결과

오징어게임
보는게 하는거 보다 재밌을 수가 없지
http://whiteship.me

이렇게 해도 Client의 코드는 변경되지 않습니다. 단지 Client인스턴스를 생성할 때 주입되는 서비스의 종류만 다를 뿐입니다. 하지만 컴파일 타임에 고정적으로 Client에 주입되는 서비스가 정해집니다. 상속을 쓰면 생각보다 유연하지 않습니다. 이상태에서 commente에 http로 시작되는 문자열을 필터링하고 싶다고 하면 또 CommentService를 상속받아서 SpamFilteringCommentService를 생성합니다.

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 {

	// 코드 생략

    public static void main(String[] args) {
        Client client = new Client(new TrimmingCommentService()); // Trimming이 추가된 서비스를 주입
        client.writeComment("오징어게임");
        client.writeComment("보는게 하는거 보다 재밌을 수가 없지...");
        client.writeComment("http://whiteship.me");
    }

}

http로 시작하는 comment가 제거된 것을 확인할 수 있습니다.

실행 결과

오징어게임
보는게 하는거 보다 재밌을 수가 없지...

하지만 trimming의 기능을 사용할 수는 없습니다. trimming을 하는 기능과 filtering을 하는 기능을 둘다 사용하고 싶다면 저기능을 합한 새로운 클래스를 생성해야할까요? 뭔가 설계가 유연하지가 않은것 같습니다. 상속자체가 유연함을 제공하지 못한다는 것을 알 수 있습니다. 이런 경우 데코레이터 패턴을 적용해서 런타임환경에서도 좀 더 유연하게 기능들을 추가할 수 있습니다.

2. 패턴 적용하기

컴포짓 패턴과 마찬가지로 Component역할을 하는 CommentService 인터페이스를 먼저 정의해보겠습니다.

public interface CommentService {

    void addComment(String comment);
}

기존에 CommnetService에서 Comment를 출력했는데 이 기능을 CommentService를 구현하는 DefaultCommentService라는 클래스로 정의합니다.

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

이렇게 Component와 ConcreteComponent가 정의되었습니다. 그 다음 Component를 감싸고 있는 CommentDecorator를 생성합니다. Decorator역시 CommentService 타입이어야 합니다. 그리고 CommentDecorator는 CommentService를 주입받고 가지고 있는 addComment를 호출만 해줍니다.

public class CommentDecorator implements CommentService {

    private CommentService commentService;

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

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

이제 Decorator를 상속받아서 기능이 추가된 TrimmingCommentDecorator와 SpamFilteringCommentDecorator를 생성합니다.

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

TrimmingCommentDecorator와 SpamFilteringCommentDecorator입장에서는 기본적으로 DefaultCommentDecorator를 감싸고 있을 수도 있고 DefaultCommentDecorator감싼 다른 Decorator수도 있는데 해당 데코레이터에서는 알 수 없고 알필요가 없습니다. 그저 해당 데코레이터에서 addComment를 재정의해서 부가기능을 추가해주기만 하면 됩니다.

이제 생성한 Decorator들을 사용할 Client클래스를 정의합니다. Client역시 CommentService타입의 인스턴스를 주입받아 코멘트를 생성하도록 합니다.

public class Client {

    private CommentService commentService;

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

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

애플리케이션에서 데코레이터 패턴을 적용한다고 가정하고 App클래스에서 Comment를 생성해 보겠습니다.

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

Client가 사용할 구체적인 CommentService인스턴스는 기본적으로는 DefaultCommentService를 사용하겠지만 만약 런타임시에 enabledSpamFilter, enabledTrimming의 값이 true라면 Decorator로 감싸서 부가기능을 런타임시에 동적으로 추가해서 Client의 생성자에 주입할 수 있습니다. 이렇게 부가기능을 추가할때 상속을 통해서 새로 클래스를 생성할 필요없이 데코레이터로 감싸기만 하면 부가기능을 유연하게 추가할 수 있습니다.

3. 결론

데코레이터 패턴은 기존 코드를 변경하지 않고 데코레이터로 감싸기만 해서 부가기능을 추가할 수 있는 패턴입니다. 새로운 클래스를 만들지 않고 기존 기능을 조합할 수 있고 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있는 장점이 있습니다. 하지만 계속 데코레이터로 감싸면서 코드가 복잡해 질 수 있는 단점이 있습니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글