관리 메뉴

나구리의 개발공부기록

프록시 패턴과 데코레이터 패턴, 프로젝트 생성 및 예제 프로젝트 만들기, 요구사항 추가, 프록시/프록시 패턴/데코레이터 패턴 소개, 프록시 패턴 - 예제, 데코레이터 패턴 - 예제, 프록시 패턴과 데코레이터 패턴 정리 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

프록시 패턴과 데코레이터 패턴, 프로젝트 생성 및 예제 프로젝트 만들기, 요구사항 추가, 프록시/프록시 패턴/데코레이터 패턴 소개, 프록시 패턴 - 예제, 데코레이터 패턴 - 예제, 프록시 패턴과 데코레이터 패턴 정리

소소한나구리 2024. 11. 7. 16:58

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프로젝트 생성 및 예제 프로젝트 만들기

1) 프로젝트 생성

(1) 프로젝트 생성 및 설정 변경

  • 강의에서 제공된 프로젝트를 복사해서 사용
  • 자바 : 17
  • 스프링 부트 : 3.3.5
  • dependency-management : 1.1.6
  • gradle : 8.10.2

2) 예제 프로젝트 v1

  • 여기에서의 v1, v2, v3는 버전업이 되는 것은 아니고 실무에서 마주하게 되는 3가지 상황에 대한 예제라고 보면 되며 v1, v2, v3의 패키지를 각각 만들어서 코드들을 작성
  • v1 - 인터페이스와 구현 클래스가 있고 스프링 빈으로 수동으로 직접 등록하는 상황

(1) OrderRepositoryV1, OrderRepositoryV1Impl

  • 로그 추적기를 만들었던 Repository의 구조와 동일한 구조로 각각의 인터페이스와 인터페이스를 구현한 구현체클래스를 생성
package hello.proxy.app.v1;

public interface OrderRepositoryV1 {
    void save(String itemId);
}

package hello.proxy.app.v1;

@Slf4j
public class OrderRepositoryV1Impl implements OrderRepositoryV1{
    @Override
    public void save(String itemId) {
        // 저장 로직
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생!");
        }
        sleep(1000);
    }

    // Thread.sleep을 try - catch하는 sleep()메서드 구현도 동일
}

 

(2) OrderServiceV1, OrderServiceV1Impl

  • 서비스코드들도 동일함
package hello.proxy.app.v1;

public interface OrderServiceV1 {
    void orderItem(String itemId);
}

package hello.proxy.app.v1;

public class OrderServiceV1Impl implements OrderServiceV1{

    private final OrderRepositoryV1 orderRepository;

    public OrderServiceV1Impl(OrderRepositoryV1 orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public void orderItem(String itemId) {
        orderRepository.save(itemId);
    }
}

 

(3) OrderControllerV1 - 인터페이스

  • 스프링 컨트롤러로 인식시키기 위해 @RestController 애노테이션을 입력, 해당 애노테이션은 인터페이스에 사용해도 됨
  • @RestController는 내부에 @ResponseBody를 가지고 있어 HTTP 메시지 컨버터를 사용해서 응답함
  • 인터페이스에서는 @RequestParam("itemId")을 생략하면 ItemId 단어를 컴파일 이후 자바 버전에 따라 인식하지 못할 수 있기 때문에 생략하지 말고 꼭 넣어주어야 하며 클래스에서 적용할 때는 생략해도 대부분 잘 지원함
package hello.proxy.app.v1;

@RestController
public interface OrderControllerV1 {

    @GetMapping("/v1/request")
    String request(@RequestParam("itemId") String itemId);

    @GetMapping("/v1/no-log")
    String noLog();
}

 

**참고

  • 이번 예제처럼 실무에서 컨트롤러를 인터페이스로 만드는 경우는 거의 없음
  • 강의에서는 스프링 부트 2.x 버전을 사용했기에 스프링 MVC가 @Controller나 @RequestMapping 애노테이션이 있어야 스프링 컨트롤러로 인식하지만 지금 실습하는 환경은 스프링 부트 3.x 버전이므로 @Controller나 @RestController 애노테이션이 있어야 스프링 컨트롤러로 인식함
  • 여기서는 REST컨트롤러로 사용할 것이기 때문에 @RestController를 적용함

(4) OrderControllerV1Impl 

  • OrderControllerV1 인터페이스를 구현한 클래스이며 로그를 찍는 @Slf4j가 여기에 붙어있음
  • 보통 컨트롤러에 컨트롤러 관련 애노테이션이 있었지만 인터페이스에 모두 적용되어 있고 클래스는 매핑 메서드가 실제 동작하도록 구현만하고 있음
package hello.proxy.app.v1;

@Slf4j
public class OrderControllerV1Impl implements OrderControllerV1 {

    private final OrderServiceV1 orderService;

    public OrderControllerV1Impl(OrderServiceV1 orderService) {
        this.orderService = orderService;
    }

    @Override
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @Override
    public String noLog() {
        return "ok";
    }
}

 

(5) AppV1Config

  • @Configuration과 @Bean 애노테이션으로 설정 클래스를 싱글톤 스프링 빈으로 수동 등록
  • Controller, Service, Repository를 의존관계 주입
package hello.proxy.config;

@Configuration
public class AppV1Config {

    @Bean
    public OrderControllerV1 orderControllerV1() {
        return new OrderControllerV1Impl(orderServiceV1());
    }

    @Bean
    public OrderServiceV1 orderServiceV1() {
        return new OrderServiceV1Impl(orderRepositoryV1());
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1() {
        return new OrderRepositoryV1Impl();
    }
}

 

(6) ProxyApplication - 수정

  • @Import로 설정 클래스를 스프링 빈으로 등록, 일반적으로 @Configuration같은 설정 파일을 등록할 때 사용하지만 스프링 빈을 등록할 때도 사용할 수 있음
  • @scanBasePackages 옵션으로 컴포넌트 스캔을 시작할 위치를 ~.app.v3 패키지와 그 하위패키지만 컴포넌트 스캔의 대상이 되도록 지정
  • app.v1과 v2는 직접 빈으로 등록하는 예제이고 v3가 컴포넌트 스캔을 사용도록 하는 예제이기 때문에 이렇게 구성하였으며 이런 사용법을 알고있으면 실무에서 도움이 될 때가 있음

** 주의

  • v1, v2패키지의 컨트롤러에서 사용하는 @RestController는 내부에 @Component를 가지고있기 때문에 컴포넌트 스캔의 대상이됨과 동시에 빈도 수동으로 직접 등록하게되면 스프링 컨테이너에서 등록시 충돌 오류가 발생함
@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3")
public class ProxyApplication {
    // ... 기존 코드 동일 생략
}

 

(7) 실행해보기

  • 코드를 적용하고 컨트롤러에 매핑된 각 url로 접속해보면 정상적으로 예제코드가 동작하는 것을 확인할 수 있음
  • v1/request에는 요청파라미터로 item에 값을 입력해주어야 정상 동작함

3) 예제 프로젝트 v2

  • v2 - 인터페이스가 없는 구체 클래스만 있고 스프링 빈으로 수동으로 직접 등록하는 상황
  • 즉 인터페이스가 없는 Controller, Service, Repository를 스프링 빈으로 수동 등록

(1) OrderRepositoryV2

  • 패키지의 위치와 클래스 명만 다르고 로직은 OrderRepositoryV1Impl과 완전히 똑같음
package hello.proxy.app.v2;

public class OrderRepositoryV2 {
    // OrderRepositoryV1Impl과 완전히 동일한 로직
}

 

(2) OrderServiceV2

  • 마찬가지로 패키지와 클래스명, 그리고 생성자로 주입받은 객체가 OrderRepositoryV2라는 것만 다르고 나머지는 동일함
package hello.proxy.app.v2;

public class OrderServiceV2 {
    // OrderRepositoV2를 선언하고 생성자로 주입받는 것만 다르고 나머지는 동일
}

 

(3) OrderControllerV2

  • OrderControllerV1의 애노테이션과 OrderControllerV1Impl의 컨트롤러 구현코드를 합쳐놓음
  • 생성자로 의존관계 주입을 받는 코드를 OrderServiceV2로 변경하고 @GetMapping의 정보를 /v2/request와 /v2/no-log로 변경
package hello.proxy.app.v2;

@Slf4j
@RestController
public class OrderControllerV2 {

    private final OrderServiceV2 orderService;

    public OrderControllerV2(OrderServiceV2 orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/v2/request")
    public String request(String itemId) {
        orderService.orderItem(itemId);
        return "ok";
    }

    @GetMapping("/v2/no-log")
    public String noLog() {
        return "ok";
    }
}

 

(4) AppV2Config 추가 및 ProxyApplication 수정

  • AppV2Config는 AppV1Config의 설정 그대로 V1의 인터페이스와 V1Impl 클래스 코드들을 전부 V2의 코드들로 수정하여 스프링 빈으로 등록
  • ProxyApplication에서는 @Import에 {} 배열을 사용하여 AppV1Config와 AppV2Config를 모두 등록하도록 설정
  • 각 설정파일의 스프링 빈으로 등록되는 이름이 각각 다르기때문에 충돌 오류가 발생하지 않음
  • 애플리케이션을 실행해보면 정상적으로 V1과 마찬가지로 정상적으로 동작함
@Import({AppV1Config.class, AppV2Config.class})
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
    // ... 코드 생략
}

4) 예제 프로젝트 v3

  • v3 - 컴포넌트 스캔으로 스프링 빈을 자동 등록하는 상황
  • 예제를 간소화하기위해 전부 구체클래스로 적용

(1) OrderRepositoryV3

  • 기존 코드와 구현 동작이 모두 동일한 OrderRepositoryV3에 @Repository 애노테이션을 적용
@Repository
public class OrderRepositoryV3 {
    // 코드 동일
}

 

(2) OrderServiceV3

  • @Service 적용, OrderRepositoryV3 주입
@Service
public class OrderServiceV3 {
    private final OrderRepositoryV3 orderRepository;
    
    // 생성자 및 기능 동일
}

 

(3) OrderControllerV3

  • V2와 완전히 동일하며 OrderServiceV3를 주입받는 코드만 변경하면 됨

(4) 실행 테스트

  • 컴포넌트 스캔으로 @RestController, @Service, @Repository를 자동으로 조회하기 때문에 수동으로 빈을 등록하지 않아도 정상적으로 동작이 되어 v1, v2, v3 모두 동작하는 예제 코드가 완성되었음

2. 요구사항 추가

1) 로그 추적기에 요구사항을 추가

(1) 로그추적기의 문제점과

  • 로그 추적기에 쓰레드 로컬과 템플릿 콜백 패턴까지 적용하면서 기존의 요구사항을 모두 만족하고 코드의 최적화까지 하는 로그 추적기를 만들었으나 결과적으로 로그 추적기를 실제 애플리케이션에 적용해야 할 때 기존 코드를 많이 수정해야함
  • 로그를 남기고 싶은 클래스가 수백개라면 수백개의 클래스의 원본 코드를 변경해야한다는 것 자체가 개발자에게는 큰 부담임

(2) 요구사항 추가

  • 원본 코드를 전혀 수정하지 않고 로그 추적기를 적용
  • 특정 메서드는 로그를 출력하지 않는 기능이 있어야 함(보안상으로 일부 로그를 출력하지 않아야 한다고 가정)
  • 인터페이스가 있는 구현클래스에도(v1), 인터페이스가 없는 구체 클래스에도(v2), 컴포넌트 스캔 대상에도(v3) 모두 적용할 수 있어야 함

(3) 문제 해결 과제

  • 가장 어려운 문제는 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입하는 것인데 이 문제를 해결하려면 프록시(Proxy)의 개념을 먼저 이해해야함

3. 프록시, 프록시 패턴, 데코레이터 패턴 - 소개

1) 프록시

(1) 클라이언트와 서버

  • 프록시의 개념을 알기 위해서는 클라이언트와 서버의 개념을 알아야함
  • 클라이언트와 서버 라고하면 보통 클라이언트는 개인컴퓨터 서버는 서버 컴퓨터를 생각할텐데 클라이언트와 서버의 개념은 상당히 넓게 사용됨
  • 클라이언트는 의뢰인이라는 뜻이고 서버는 서비스나 상품을 제공하는 사람이나 물것을 뜻함
  • 즉, 클라이언트는 서버에 필요한 것이고 서버는 클라이언트의 요청을 처리하는 것임
  • 이러한 개념을 컴퓨터 네트워크에 도입하면 클라이언트는 웹 브라우저가 되고 요청을 처리하는 서버는 웹 서버가 되며 객체에 도입하면 요청하는 객체는 클라이언트가 되고 요청을 처리하는 객체는 서버가 됨

(2) 직접 호출과 간접 호출

좌) 직접 호출 / 우) 간접 호출

  • 클라이언트와 서버 개념에서 일반적으로는 클라이언트가 서버를 직접 호출하고 처리 결과를 직접 받는데 이것을 직접 호출이라함
  • 그러나 클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해 간접적으로 서버에 요청할 수 있는데 이 대리자를 영어로 프록시(Proxy)라고함

(3) 대체 가능, 서버와 프록시가 같은 인터페이스를 사용

 

클라이언트 프록시, 서버의 클래스 의존관계

  • 프록시는 아무 객체가 될 수 있는 것은 아니며 객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지 프록시에게 요청을 한 것인지 조차 몰라야함
  • 즉, 서버와 프록시는 같은 인터페이스를 사용해야하며 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 함
  • 클라이언트는 서버 인터페이스에만 의존하고 서버와 프록시가 같은 인터페이스를 사용하기에 DI를 사용해서 대체가 가능함
  • 런타임(애플리케이션 실행)시점에 클라이언트 객체에 DI를 사용해서 클라이언트가 서버에서 프록시로 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 되며 클라이언트 입장에서는 변경된 사실조차 모름
  • DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있음

(4) 프록시의 주요기능

  • 프록시 객체가 중간에 있으면 크게 접근 제어와 부가 기능 추가를 수행할 수 있음
  • 접근제어: 권한에 따른 접근 차단, 캐싱, 지연 로딩
  • 부가 기능 추가 : 원래 서버가 제공하는 기능에 대해서 부가 기능을 수행
  • 부가 기능 추가 예시 - 요청 값이나 응답 값을 중간에 변형, 실행 시간을 측정해서 추가 로그 시간을 남김 등
  • 프록시가 또 다른 프록시를 호출하는 프록시 체인의 구조로 적용할 수 있으며 클라이언트는 대리자에게 요청이 가면 그 이후의 요청은 모르고 결과만 나에게 반환되면 됨

(5) GOF 디자인 패턴

  • 둘다 프록시를 사용하는 방법이지만 GOF 디자인 패턴에서는 이 둘의 의도(intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분
  • 프록시 패턴 : 접근 제어가 목적
  • 데코레이터 패턴 : 새로운 기능 추가가 목적
  • 둘다 프록시를 사용하지만 의도가 다르다는 점이 핵심이며 용어가 프록시 패턴이라고해서 이 패턴만 프록시를 사용하는 것이 아니라 데코레이터 패턴도 프록시를 사용함

** 참고

  • 프록시 라는 개념은 클라이언트 서버라는 큰 개념안에서 자연스럽게 발생할 수 있음
  • 프록시는 객체 안에서의 개념도 있고 웹 서버에서의 프록시도 있기에 객체 안에서 객체로 구현되어있는지, 웹 서버로 구현되어 있는지의 규모의 차이가 있을 뿐, 근본적인 역할은 같음

4. 프록시 패턴 - 예제 코드

1) 예제1 - 프록시 사용 X

(1) 관계도

 

(2) Subject 인터페이스와 Subject를 구현하는 RealSubject 구현체

  • 예제 코드는 모두 test의 하위 패키지에 pureproxy.proxy.code의 패키지 구조를 만들어서 적용
  • Subject인터페이스는 operation()이라는 기능 하나만 가지고있음
  • RealSubject는 Subject를 구현하여 실제 객체 호출이라는 로그를 출력 후 1초 대기하고 반환하도록 operation()메서드를 구현
  • 즉 데이터를 DB나 외부에서 조회하는데 1초가 걸린다고 가정하여 호출할 때마다 시스템에 큰 부하를 주는 데이터 조회라고 가정함
package hello.proxy.pureproxy.proxy.code;

public interface Subject {
    String operation();
}

package hello.proxy.pureproxy.proxy.code;

@Slf4j
public class RealSubject implements Subject {
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
           e.printStackTrace();
        }
    }
}

 

(3) ProxyPatternClient

  • Subject인터페이스에 의존하고 Subject를 호출하는 클라이언트 코드
  • execute()를 실행하면 subject.operation()를 호출함
package hello.proxy.pureproxy.proxy.code;

public class ProxyPatternClient {
    private Subject subject;

    public ProxyPatternClient(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.operation();
    }
}

 

(4) ProxyPatternTest 작성 및 실행

  • 아직 프록시를 적용하지 않는 테이스케이스로 클라이언트를 생성할 때 생성한 RealSubject객체를 의존관계 주입
  • 클라이언트가 execute()메서드 요청을 3번 연속하도록 작성 후 실행해보면 작성한 코드의 동작대로 1초에 한번씩 로그가 출력됨
package hello.proxy.pureproxy.proxy;

public class ProxyPatternTest {

    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPatternClient client = new ProxyPatternClient(realSubject);

        client.execute();
        client.execute();
        client.execute();
    }
}

 

(5) 개선점

  • 그런데 이 데이터가 한번 조회할 때 변하지 않는 데이터라면 어딘가에 보관해두고 이미 조회한 데이터를 사용하는 것이 성능상에는 분명히 좋을텐데, 이런 것을 캐시라고 함
  • 프록시 패턴의 주요 기능은 접근 제어인데 캐시도 접근 자체를 제어하는 기능 중 하나이며 이를 통해 성능개선을 할 수 있음

2) 예제1 - 프록시 패턴 적용

(1) 관계도

  • 런타임 시점에 클라이언트가 프록시를 의존함

(2) CacheProxy 추가

  • Subject 인터페이스를 구현하는 CacheProxy클래스를 생성
  • 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야하기에 내부에 실제 객체의 참조를 가지고 있어야함
  • 프록시가 호출하는 대상을 target이라고 하기에 실제객체 타입으로 target 변수를 선언
  • operation() 메서드에서 cacheValue에 값이 없으면 실제 객체의 operation()메서드를 호출해서 값을 반환하고 cacheValue에 값이 있으면 cacheValue실제 객체를 전혀 호출하지 않고 캐시 값을 그대로 반환함
  • 즉, 처음 조회 이후에는 캐시에서 매우 빠르게 데이터를 조회할 수 있게됨
package hello.proxy.pureproxy.proxy.code;

@Slf4j
public class CacheProxy implements Subject {

    private Subject target;     // 실제 객체
    private String cacheValue;  // 캐시 값

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("프록시 호출");
        if (cacheValue == null) {   // cacheValue가 없으면 실제 객체의 메서드를 호출
            cacheValue = target.operation();
        }
        return cacheValue;          // 있으면 cacheValue값을 출력
    }
}

 

(3) ProxyPatternTest 테스트 추가 및 실행

  • cacheProxyTest()를 추가하여 프록시를 사용하는 테스트케이스를 생성
  • 클라이언트는 CacheProxy를 CachProxy는 RealSubject를 의존관계 주입을 받으면서 client -> cacheProxy -> realsubject  런타임 객체 의존관계가 완성됨
  • client가 execute()를 3번 연속으로 호출하면 실제 객체가아닌 캐시 프록시를 호출하게 되면서 최초에만 실제 객체를 호출하여 1초의 소요시간이 걸리고 그다음 호출 2번은 프록시에서 바로 값을 꺼내오는 결과가 출력됨
@Test
void cacheProxyTest() {
    RealSubject realSubject = new RealSubject();
    CacheProxy cacheProxy = new CacheProxy(realSubject);
    ProxyPatternClient client = new ProxyPatternClient(cacheProxy);

    client.execute();
    client.execute();
    client.execute();
}

1번째 execute()만 1초가 걸리고 나머지는 0초만에 전부 호출됨

(4) 정리

  • 결과적으로 캐시 프록시를 도입하기 전에는 3초가 걸렸지만, 캐시 프록시 도입 이후에는 최초에 한번만 1초가 걸렸고 이후에는 거의 즉시 반환하였음
  • 프록시 패턴의 핵심은 RealSubject코드와 클라이언트 코드를 전혀 변경하지 않았고 프록시를 도입하여 접근 제어를 했다는 점임
  • 클라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있으며 실제 클라이언트 입장에서는 프록시 객체가 주입이 되었는지 실제 객체가 주입 되었는지 알지 못함

5. 데코레이터 패턴 - 예제 코드

1) 예제1 - 데코레이터 패턴 사용 X

(1) 관계도

(2) Component인터페이스와 RealComponent 구현체

  • test하위에 생성하였던 pureproxy하위에 decorator.code 패키지를 생성하여 코드를 작성
  • 로직은 프록시 패턴 테스트와 거의 동일하며 sleep()메서드만 없음
package hello.proxy.pureproxy.decorator.code;

public interface Component {
    String operation();
}

package hello.proxy.pureproxy.decorator.code;

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

 

(3) DecoratorPatternClient

  • 클라이언트코드는 단순히 Component 인터페이스를 의존하고 execute()메서드를 실행하면 component.operation()메서드를 실행하고 그 결과를 출력함
package hello.proxy.pureproxy.decorator.code;

@Slf4j
public class DecoratorPatternClient {

    private Component component;

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

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

 

(4) DecoratorPatternTest

  • 데코레이터 패턴이 적용되지 않는 테스트케이스의 클라이언트로 execute()메서드를 호출
  • 클라이언트가 실제객체인 RealComponent를 주입받아 execute()메서드를 호출하고 있으며 앞에서 했던 프록시 패턴에서의 프록시를 적용하지 않은 코드와 유사한 구조의 어렵지 않은 코드임
package hello.proxy.pureproxy.decorator;

@Slf4j
public class DecoratorPatternTest {

    @Test
    void noDecorator() {
        Component realComponent = new RealComponent();
        DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
        client.execute();
    }
}

2) 예제2 - 데코레이터 패턴 적용1

(1) 부가 기능 추가와 관계도

  • 프록시를 통해서 할 수 있는 기능은 크게 접근 제어와 부가 기능 추가라는 2가지로 구분하는데 프록시를 활용해서 부가 기능을 추가하는 것을 데코레이터 패턴이라고 함
  • 응답 값을 꾸며주는 데코레이터 프록시를 생성

(2) MessageDecorator

  • 데코레이터 역할을 수행하는 클래스를 생성하여 Component를 구현하고, 실제 객체를 호출한 결과를 꾸며주는 로직을 추가하여 operation()메서드를 구현
  • 꾸미기 전에는 data, 꾸민 후에는 ****data****이 출력됨
package hello.proxy.pureproxy.decorator.code;

@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();          // 실제 객체의 operation() 메서드 호출
        String decoResult = "****" + result + "****";   // 응답값에 앞,뒤로 ****을 붙혀서 반환
        log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
        return decoResult;
    }
}

 

(3) DecoratorPatternTest 추가 및 실행

  • client -> messageDecorator -> realComponent의 객체 의존관계가 완성되고 client.execute()를 호출하면 데코레이터의 메서드 연산결과가 출력됨
@Test
void decorator1() {
    Component realComponent = new RealComponent();
    MessageDecorator decorator = new MessageDecorator(realComponent);
    DecoratorPatternClient client = new DecoratorPatternClient(decorator);
    client.execute();
}

데코레이터가 적용된 출력 결과

3) 예제3 - 데코레이터 패턴 적용2

(1) 실행 시간을 측정하는 데코레이터 추가

  • 기존 데코레이터에 기능을 더해서 실행 시간을 측정하는 기능을 추가
  • 프록시는 체인이 될 수 있기 때문에 데코레이터 패턴에서 체인을 적용하면 부가 기능을 계속 추가할 수 있음

 

(2) TimeDecorator 추가

  • messageDecorator를 생성했던것과 동일한 방식으로 구현
  • operation메서드를 구현 시 실행시간을 출력하는 로직을 추가
package hello.proxy.pureproxy.decorator.code;

@Slf4j
public class TimeDecorator implements Component {

    private Component component;

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

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");
        long startTime = System.currentTimeMillis();
        
        String result = component.operation();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeDecorator 종료 resultTime={}ms", resultTime);

        return result;
    }
}

 

(3) DecoratorPatternTest 추가 및 실행

  • 지금까지 만든 부가기능을 client -> timeDecorator -> messageDecorator -> realComponent의 객체 의존관계를 설정 후 메서드를 실행
  • 즉, 프록시가 프록시를 호출하는 프록시 체인을 사용해서 부가기능을 추가하였고 클라이언트는 최종결과만 받아서 사용함
  • 부가기능이 모두 적용된 연산결과가 출력되는 것을 확인할 수 있음
@Test
void decorator2() {
    Component realComponent = new RealComponent();
    MessageDecorator decorator = new MessageDecorator(realComponent);
    TimeDecorator timeDecorator = new TimeDecorator(decorator);
    DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
    client.execute();
}


6. 프록시 패턴과 데코레이터 패턴 정리

1) GOF 데코레이터 패턴

(1) 모양

(2) 설명

  • 위 모양은 GOF에서 설명하는 데코레이터 패턴의 기본 예제이며, 지금까지 구현한 것은 위 모양에서 Decorator를 생략하고 바로 Component로 데코레이터들을 구현한 예제이며 둘다 데코레이터 패턴임
  • 구현한 데코레이터들의 코드를 잘 보면 기능에 일부 중복이 있는데 꾸며주는 역할을 하는 데코레이터들은 스스로 존재하지 않고 항상 꾸며줄 대상이 필요하기에 내부에서 호출 대상인 Component를 가지고 있어야하며 항상 Component를 호출해야함
  • 이부분이 중복인데 이런 중복을 제거하기 위해 component를 위의 모양처럼 Decorator라는 추상 클래스를 만드는 방법도 고민할 수 있음
  • 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지, 데코레이터인지 명확하게 구분할 수 있으며 여기까지 고민한 것이 바로 GOF에서 설명하는 데코레이터 패턴임

(3) 프록시 패턴 vs 데코레이터 패턴

  • 프록시 패턴과 데코레이터 패턴은 모양이 거의 비슷함
  • 위 두가지 패턴은 모양이 거의 같거나 상황에 따라 정말 똑같을 때도 있는데 디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도(intent)가 더 중요하기에 의도에 따라 패턴을 구분함
  • 앞서 설명했듯 프록시 패턴의 의도는 다른 객체에 대한 접근을 제어하기 위해 대리자를 제공하는 것이고 데코레이터 패턴의 의도는 객체에 추가 책임(기능)을 동적으로 추가하고 기능 확장을 위한 유연한 대안을 제공하는 것임
  • 즉, 프록시를 사용하고 해당 프록시가 접근 제어가 목적이면 프록시 패턴이며, 새로운 기능을 구하는 것이 목적이면 데코레이터 패턴이 되면 두가지 패턴을 모두 한번에 적용하여 사용할 수도 있음