본문 바로가기
아키텍쳐

데코레이터 패턴

by sangyunpark99 2025. 3. 6.
데코레이터 패턴이 뭘까요?

 

 

이번 글은 프록시와 데코레이터 패턴에 대해 정리한 글입니다.

참고 강의 : 김영한의 스프링 핵심 원리 - 고급편

 

 

데코레이터 패턴

데코레이터 패턴은 객체 지향 디자인 패턴 중 하나로, 기존 객체의 구조를 변경하지 않고, 동적으로 새로운 기능을 추가할 수 있는 방법을 제공합니다. 

 

어떻게 기존 객체의 구조를 변경하지 않고, 동적으로 새로운 기능을 추가할 수 있을까요?

 

하나의 예를 들어, 데코레이터 패턴 적용전의 의존 관계와 적용 후 의존관계를 통해 알아보겠습니다.

 

먼저, 데코레이터 패턴 적용전의 클래스 의존 관계는 다음과 같습니다.

클래스 의존관계

 

데코레이터 패턴 적용전의 런타임 객체 의존 관계는 다음과 같습니다.

런타임 의존관계

의존관계를 코드로 확인해보겠습니다.

 

 

Component 인터페이스

public interface Component {
    String operation();
}

 

Component 인터페이스를 구현한 RealComponent

@Slf4j
public class RealComponent implements Component{
    @Override
    public String operation() {
        log.info("RealComponent 실행");
        return "data";
    }
}

 

Component 인터페이스에 의존하는 Client 클래스

@Slf4j
public class Client {

    private Component component;

    public DecoratorPatternClient(Component component) {
        this.component = component;
    }

    public void execute() {
        String result = component.operation();
        log.info("result={}", result);
    }
}

 

 

Client의 TestCode

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        Client client = new Client(realComponent);

        client.execute();
    }
}

 

Client 클래스에 Component 인터페이스를 구현한 realComponent 객체를 주입해줍니다.

client.execute() 메서드를 호출하면, realComponent의 operation() 메서드가 호출됩니다.

 

출력 결과

 

 

이제, 프록시를 사용해서 데코레이터 패턴을 적용시켜 보겠습니다.

 

프록시를 통해서 할 수 있는 것은 접근 제어, 부가 기능 추가를 할 수 있습니다.

이번엔 프록시를 활용해서 부가 기능 추가를 해보겠습니다. 부가 기능은 메서드 호출의 응답 값인 문자열을 꾸며주는 것입니다.

 

먼저, 데코레이터 패턴을 적용한 후의 클래스 의존 관계를 살펴보겠습니다.

클래스 의존관계

 

런타임 의존 관계는 다음과 같습니다.

데코레이터 패턴을 적용한 코드를 통해 의존 관계를 알아보겠습니다. 

 

Component 클래스와 구현체인 RealComponent 클래스는 코드가 동일합니다.

 

Component를 구현하는 MessageDecorator 클래스는 다음과 같습니다.

@Slf4j
public class MessageDecorator implements Component{

    private Component component;

    public MessageDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {

        log.info("MessageDecorator 실행");

        String result = component.operation();

        String decoResult = "😊😊😊😊😊" + result + "😊😊😊😊😊";

        log.info("MessageDecorator 꾸미기 전 = {}, 꾸민 후 = {}", result, decoResult);

        return decoResult;
    }
}

 

필드 변수에 Compont 인터페이스를 선언하고, operation()메서드 내에서 생성자 주입을 받은 Component의 구현체의 operation() 메서드를 실행한 결과를 꾸며주고 있습니다.

 

테스트 코드를 확인해서 응답값을 잘 꾸며주는지 확인 해보겠습니다.

@Test
void Decorator() {
    Component realComponent = new RealComponent();
    MessageDecorator messageDecorator = new MessageDecorator(realComponent);
    Client client = new Client(messageDecorator);

    client.execute();
}

 

 

출력 결과는 다음과 같습니다.

 

기존 응답값은 "data"이지만, 데코레이터 패턴을 사용해서 "😊😊😊😊😊data😊😊😊😊😊"로 변경했습니다.

 

실행 흐름을 살펴보겠습니다.
Client client = new Client(messageDecorator);
client.execute();

 

Client 클래스를 생성할때, 프록시 객체인 messageDecorator를 주입해줍니다.

 

 

Client 클래스에서 필드 변수 component는 messageDecorator가 됩니다.

따라서, client.execute() 메서드를 호출하면 messageDecorator의 operation() 메서드가 호출됩니다.

 

Component realComponent = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realComponent);

 

MessageDecorator 클래스를 생성할때, realComponent 객체를 주입해줍니다.

 

따라서, MessageDecorator 클래스 내부의 component 필드 변수는 realComponent 생성자로 주입받은 realComponent 객체가 되고, MessageDecorator의 operation() 메서드에서 호출한 component의 operation()메서드는 곧 realComponent의 operation() 메서드를 호출하게 됩니다.

 

@Slf4j
public class RealComponent implements Component{
    @Override
    public String operation() {
        log.info("RealComponent 실행");
        return "data";
    }
}

 

realComponent의 operation() 메서드를 호출하는 경우 "data"라는 문자열이 return 됩니다.

 

메서드가 호출되는 흐름은 client.execute() → messageDecorator.operation() → realComponent.operation()이 됩니다.

 

데코레이터를 한개 더 추가해 볼까요?

 

실행 시간을 추가해주는 데코레이터를 구현해보겠습니다.

@Slf4j
public class TimeDecorator implements Component {

    private Component component;

    public TimeDecorator(Component component) {
        this.component = component;
    }

    @Override
    public String operation() {
        long startTime = System.currentTimeMillis();
        String result = component.operation();
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("resultTime = {}ms", resultTime);

        return result;
    }
}

 

Component를 구현하는 TimeDecorator 클래스를 선언해줍니다.

 

@Test
void DecoratorWithTime() {
    RealComponent realComponent = new RealComponent();
    MessageDecorator messageDecorator = new MessageDecorator(realComponent);
    TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
    Client client = new Client(timeDecorator);

    client.execute();
}

 

TimeDecorator를 적용 해줍니다.

 

메서드 호출 흐름은 다음과 같습니다.

client.execute() → timeDecorator.operation() messageDecorator.operation() → realComponent.operation()

 

출력 결과는 다음과 같습니다.

 

 

GOF 데코레이터 패턴

GOF에서 제공하는 데코레이터 패턴의 기본 예제를 정의한 그림은 아래와 같습니다.

 

Decrator클래스는 꾸며줄 대상이 항상 존재해야 합니다. 흐름도에서 꾸며주는 대상은 component를 의미합니다. 

이말은 곧 모든 Decrator가 component를 호출하는 코드가 중복됨을 의미합니다. 이러한 중복 코드를 제거하기 위해선 component를 속성으로 가지는 Decorator 추상 클래스를 만들어 중복 코드를 방지해줄 수 있습니다.

 

 

 

정리

  • 데코레이터 패턴을 사용하면 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안을 제공합니다.
  • 프록시 객체를 사용하고, 접근 제어가 목적인 경우엔 프록시 패턴을 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴을 사용합니다.

'아키텍쳐' 카테고리의 다른 글

Saga Pattern(사가 패턴)  (0) 2025.04.11
DDD(도메인 주도 설계)  (0) 2025.03.24
프록시 & 프록시 패턴  (0) 2025.03.05
API 멱등성  (0) 2025.02.02
VO(Value Obejct)  (0) 2025.01.31