Ian's Archive 🏃🏻

Profile

Ian

Ian's Archive

Developer / React, SpringBoot ...

📍 Korea
Github Profile →
Categories
All PostsAlgorithm19Book1C1CI/CD2Cloud3DB1DesignPattern9ELK4Engineering1Front3Gatsby2Git2IDE1JAVA7JPA5Java1Linux8Nginx1PHP2Python1React9Security4SpatialData1Spring26
thumbnail

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

DesignPattern
2025.11.17.

1. 데코레이터 패턴

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

  • 객체의 결합을 통해 기능을 동적으로 유연하게 확장할 수 있게 해주는 패턴
    • 동적으로 유연하게 확장이 가능하다 -> 런타임 시 부가기능을 추가하는 것도 가능하다는 의미
    • 객체의 결합이란 상속이 아닌, 위임 즉, Has-a관계를 가지는 패턴을 의미
    • 데코레이터 패턴은 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계하는 패턴

Tip! - 합성 vs 위임

합성

객체 안에 다른 객체를 포함하는 구조

복사
class A {
    B b;  // 포함 = 합성
}

위임

포함된 그 객체에게 일을 넘겨서 실행하게 만드는 행동 방식

복사
class A {
    B b;

    void doSomething() {
        b.doSomething();  // 위임(delegate)
    }
}

  • Component : 원본 객체와 장식된 객체 모두를 묶는 역할
  • ConcreateComponent : 원본 객체
  • Decorator : 추상화 된 장식자 클래스
    • 원본 객체를 합성(Composition)한 wrapper 필드와 인터페이스의 구현 메소드를 가짐
  • ConcreateDecorator : 구체적인 장식자 클래스
    • 부모 클래스가 감싸고 있는 하나의 Component를 호출하면서 호출 전/후로 부가적인 로직을 추가

2

  • Decorator는 Component를 멤버 변수로 포함(=합성) 하여 감싸고, Component가 가진 기능(Operation)을 그대로 제공한다.
  • 기능 확장 시 Component를 직접 상속하지 않고 Decorator를 상속함으로써 동일한 인터페이스 구조에서 기능을 추가할 수 있다.
  • 즉, Decorator는 부모 인터페이스(Component)를 내부에 가지고 있어 외부에서 의존성 주입이 가능하고, 기능을 확장한 Decorator(자식)는 이 부모 기능을 활용해 기존 기능 + 확장 기능을 만들 수 있다.

=> 결과적으로 확장된 Decorator를 외부에서 조립(주입)하면서 기능을 점진적으로 덧붙이는 구조가 된다.

즉, 데코레이터는 내부에 원본 객체를 합성(composition) 방식으로 들고 있고, 메서드 호출은 그 원본 객체에 위임한다.

복사
class ToppingDecorator implements IceCream {
    protected IceCream inner; // 합성

    @Override
    public String describe() {
        return inner.describe(); // 위임
    }
}

즉, 데코레이터 = 합성 + 위임 + 기능 덧입히기

2. 데코레이터 패턴 구현

  • 상속으로 기능을 확장 -> 데코레이터 패턴 적용

상속으로 기능 확장 예시

복사
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);
    }
}

public class TrimmingCommentService extends CommentService{

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

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

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 객체를 조합함으로써 추가 기능의 조합을 설계하는 데코레이터 패턴을 사용

데코레이터 패턴 적용 예시

복사
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("내가임마!!! 어이!!");
    }
}
  • CommentService를 인터페이스를 기능을 분리
복사
public class CommentDecorator implements CommentService {

    private CommentService commentService;

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

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

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");
    }
}
  • 확장된 기능을 Overriding 한 각각 구현체에서 부모의 기능을 참조하여 기능을 확장
복사
/**
 *  데코레이터패턴을 사용하여 상속이 아닌 위임으로 문제를 해결
 */
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("내가임마!!! 어이!!");

    }
}

3. 패턴 사용시기와 장, 단점

패턴 사용 시기

  • 객체 책임과 행동이 동적으로 상황에 따라 다양한 기능이 빈번하게 추가/삭제 되는 경우
  • 객체의 결합을 통해 기능이 생성될 수 있는 경우
  • 객체를 사용하는 코드를 손상시키지 않고 런타임에 객체에 추가 동작을 할당할 수 있어야 하는 경우
  • 상속을 통해 서브 클래싱으로 객체 동작을 확장하는 것이 어색하거나 불가능 할 때

장점

  • 서브클래스를 만들 때 보다 더 유연하게 기능 확장
  • 객체를 여러 데코레이터로 래핑해 여러 동작 결합 가능
  • 컴파일 타임이 아닌 런타임에 동적으로 기능 변경 가능
  • 장식자 클래스마다 고유의 책임을 가져 단일책임원칙(SRP)를 준수
  • 클라이언트 코드 수정 없이 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP) 준수
  • 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP) 준수

단점

  • 장식자 일부를 제거하고 싶으면, Wrapper 스택에서 특정 wrapper를 제거하는 것이 어렵다
  • 데코레이터를 조합하는 초기 생성 코드가 보기 안좋을 수 있다. (new A(new B(new C())))
  • 순서에 따라 데코레이터 스택 순서가 결정되기에 순서에 의존하지 않는 방식으로 데코레이터를 구현하기 어렵다

4. 실무에서 찾아보는 Decorator 패턴

Java

  • InputStream, OutputStream, Reader, Writer의 생성자를 활용한 랩퍼
  • java.util.Collections가 제공하는 메소드들 활용한 랩퍼
  • javax.servlet.http.HttpServletRequest / ResponseWrapper
  • java.io.InputStream, OutputStream, Reader, Writer의 모든 하위 클래스에 동일한 유형의 인스턴스를 사용하는 생성자
  • java.util.Collections의 checkedXXX(), synchronizedXXX(), unmodifiableXXX() 메서드들
  • javax.servlet.http.HttpServletRequestWrapper 그리고 HttpServletResponseWrapper
  • javax.swing.JScrollPane

자바 I/O 메서드

  • InputStream, OutputStream, Reader, Writer의 생성자를 활용한 파일 I/O 랩퍼 부분은 데코레이터 패턴의 대표적인 예
  • 자바 코드에서 파일을 읽어 들일 때 다음과 같이 객체 생성자를 중첩하여 사용
  • File → FileReader → BufferedReader 순으로 갈수록 점점 부가 기능이 추가 됨

Spring Framework  

  • HttpServletRequestWrapper / HttpServletResponseWrapper
    • HttpServletRequest를 확장해서 HttpServletRequestWrapper가 제공하는 기능을 오버라이딩해서 부가적인 기능을 추가 가능
      • 예를들어 HTTP 요청 메시지 본문을 다르게 처리. 캐싱, 로깅, 의심스러운 요청 확인 등의 작업을 해야할 때, 이런 wrapper를 만들어서 사용
  • BeanDefinitionDecorator
    • 빈(Bean) 설정 데코레이터로 스프링의 인프라로 등록
  • ServerHttpRequestDecorator / ServerHttpResponseDecorator
    • Webflux HTTP 요청/응답 데코레이터
    • 이 데코레이터를 상속받는 클래스를 만들어서 WebFilter를 거쳐가는 모든 요청이 이 데코레이터의 하위 클래스를 거쳐가게 함

Reference

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

Previous Post
브릿지 패턴 정리
Next Post
데이터 플랫폼 설계와 구축을 읽고
Thank You for Visiting My Blog, I hope you have an amazing day 😆
© 2023 Ian, Powered By Gatsby.