데코레이터(Decorator) 패턴

데코레이터 패턴은 객체에 동적으로 기능을 추가할 수 있는 디자인 패턴입니다.
이 패턴은 상속을 통해 기능을 확장하는 대신, 객체를 감싸는 데코레이터 클래스를 사용하여 기능을 추가하고 조합합니다.
이를 통해 코드의 유연성과 확장성을 향상시킬 수 있습니다.

구성 요소

컴포넌트(Component): 기본 기능을 정의하는 인터페이스 또는 추상 클래스입니다.
데코레이터(Decorator): 컴포넌트를 감싸는 역할을 하며, 컴포넌트와 동일한 인터페이스를 구현합니다.
구체적인 데코레이터(Concrete Decorator): 데코레이터를 상속받아 새로운 기능을 추가하거나 기존 기능을 수정합니다.

구현:

  1. 컴포넌트 인터페이스를 정의합니다. 이 인터페이스는 기본 기능을 정의합니다.
  2. 기본 기능을 구현하는 구체적인 컴포넌트 클래스를 생성합니다.
  3. 데코레이터 클래스를 생성하여 컴포넌트를 감싸고, 추가 기능을 정의합니다. 이 클래스는 컴포넌트와 동일한 인터페이스를 구현합니다.
  4. 구체적인 데코레이터 클래스를 생성하여 데코레이터를 상속받아 새로운 기능을 추가하거나 기존 기능을 수정합니다.
  5. 클라이언트에서는 원하는 기능의 조합을 위해 데코레이터를 사용하여 컴포넌트를 감싸고, 기능을 호출합니다.

장점

객체에 동적으로 기능을 추가할 수 있습니다.
상속을 통해 기능을 확장하는 대신, 객체를 감싸는 데코레이터 클래스를 사용하여 기능을 추가하고 조합할 수 있습니다.
기존 코드의 수정 없이 기능을 추가하거나 수정할 수 있습니다.

단점

객체가 많이 중첩될 경우 데코레이터 클래스의 수가 증가하여 코드 복잡성이 증가할 수 있습니다.

예시

Comment를 추가하는 서비스가 있고, 옵션에 따라 url 차단 기능과 trim 기능을 선택적으로 제공하려고한다.

Component

public interface CommentService {
    void addComment(String comment);

    List<String> getCommentList();
}

Default Component

public class DefaultCommentService implements CommentService {
    private final List<String> commentList = new ArrayList<>();

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

    @Override
    public List<String> getCommentList() {
        return commentList;
    }
}

Decorator

public abstract class CommentServiceDecorator implements CommentService{

    protected final CommentService commentService;

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

    @Override
    public final List<String> getCommentList() {
        return commentService.getCommentList();
    }
}

Concrete Decorator

public class SpamFilteringCommentServiceDecorator extends CommentServiceDecorator {

    public SpamFilteringCommentServiceDecorator(CommentService commentService) {
        super(commentService);
    }

    @Override
    public void addComment(String comment) {
        boolean isSpam = isSpam(comment);
        if (!isSpam) {
            this.commentService.addComment(comment);
        }
    }


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

public class TrimmingCommentServiceDecorator extends CommentServiceDecorator {

    public TrimmingCommentServiceDecorator(CommentService commentService) {
        super(commentService);
    }

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

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

}

Client

public class CommentServiceTest {

    // 파라미터 개수만큼 테스트를 진행
    @ParameterizedTest
    // isSpamFilteringWithIsTrimming 리턴값을 파라미터로 사용하도록 지정
    @MethodSource("isSpamFilteringWithIsTrimming")
    public void comment_test(boolean isEnabledSpamFiltering, boolean isEnabledTrimming) {
        CommentService commentService = new DefaultCommentService();

        // isEnabledSpamFiltering이 true면 url 코멘트 삭제
        if(isEnabledSpamFiltering){
            commentService = new SpamFilteringCommentServiceDecorator(commentService);
        }
        // isEnabledTrimming이 true면 comment trim
        if(isEnabledTrimming){
            commentService = new TrimmingCommentServiceDecorator(commentService);
        }

        List<String> commentList = getCommentList(commentService);

        commentList.stream().forEach(System.out::println);

        assertTrue(commentList.contains("오징어게임"));
        if(isEnabledTrimming){
            assertTrue(commentList.contains("보는게 하는거 보다 재밌을 수가 없지"));
        }else{
            assertTrue(commentList.contains("보는게 하는거 보다 재밌을 수가 없지..."));
        }

        if(!isEnabledSpamFiltering){
            assertTrue(commentList.contains("http://kktrkkt.github.io"));
        }

    }

    private static Stream<Arguments> isSpamFilteringWithIsTrimming() {
        return Stream.of(
                Arguments.of(true, true),
                Arguments.of(true, false),
                Arguments.of(false, true),
                Arguments.of(false, false)
        );
    }

    private static List<String> getCommentList(CommentService commentService) {
        commentService.addComment("오징어게임");
        commentService.addComment("보는게 하는거 보다 재밌을 수가 없지...");
        commentService.addComment("http://kktrkkt.github.io");

        return commentService.getCommentList();
    }
}

결론

데코레이터 패턴은 객체에 동적으로 기능을 추가할 수 있는 유연한 디자인 패턴입니다.
상속을 통해 기능을 확장하는 대신, 객체를 감싸는 데코레이터 클래스를 사용하여 기능을 추가하고 조합합니다.
이를 통해 코드의 유연성과 확장성을 향상시킬 수 있습니다. 단, 객체가 많이 중첩될 경우 데코레이터 클래스의 수가 증가하여 코드 복잡성이 증가할 수 있습니다.

예제는 이곳 에서 확인하실수 있습니다.