WEB/Spring

스프링 핵심 원리 - 고급편 4) 프록시 패턴과 데코레이터 패턴

Tony Lim 2022. 6. 6. 15:29
728x90

예제 생성할때 주의할점

@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

원래는 해당 ProxyApplication이 존재하는 패키지 부터 (proxy) 밑으로 쫘악 component scan을 하지만 여기서는 Configuration을 계속해서 바꾸길 원하기 때문에 basepackage의 위치를 변경하였다.

 

@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();
    }

}

이와 같이 Configuration에서 Bean으로 수동등록을 하기에

@RequestMapping//스프링은 @Controller 또는 @RequestMapping 이 있어야 스프링 컨트롤러로 인식
@ResponseBody
public interface OrderControllerV1 {

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

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

@Controller 을 쓰지 않는다. @RequestMapping 은 @Component가 존재하지 않아서 component-scan 대상에 들어가지 않는다.

@Controller 는 @Componet를 가지고있기에 componet-scan 대상이 된다.

 


 

프록시

클라이언트 서버 관계에서 중간에 프록시라는 대리자가 끼게 된다.

1. 접근제어와 캐싱 기능

2. 실제 로직을 호출하기전에 부가기능을 추가

실제 서버 처럼 대체가능 해야 한다.

 

의도에 따라

프록시 패턴 , 데코레이터 패턴으로 구분하게 된다.

프록시 패턴 = 접근제어가 목적

데코레이터 패턴 = 새로운 기능 추가가 목적


 

캐시기능이 담긴 프록시패턴 예제 를 만들어봅세

public interface Subject {
    String operation();
}
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();
        }
    }
}
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 = target.operation();
        }
        return cacheValue;
    }
}

Subject라는 인터페이스 를 프록시 클래스도 구현하고 있고 실제 객체역할을 담당할 클래스도 구현을 하고 있다.

프록시에서 캐시가 비어있으면 실제 객체를 호출하고 캐시가 존재하면 해당 값을 반환한다.

이런 관계를 만들기 위함이다.

public class ProxyPatternClient {

    private Subject subject;

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

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

클라이언트는 주입받은 Subject의 메소드를 호출할 뿐 안에서 뭘하는지 알 수 없는 상황이다.

void cacheProxyTest() {
    RealSubject realSubject = new RealSubject();
    CacheProxy cacheProxy = new CacheProxy(realSubject);
    ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
    client.execute();
    client.execute();
    client.execute();
}

client -> cacheProxy -> realSubject 관계가 형성된다.

ProxyPatternClient를 전혀 손대지 않고 접근제어 (캐싱) 기능이 추가 되었다.

 


 

데코레이터 패턴 예제

동일하게 프록시를 사용하지만 의도에 따라 다르게 부른다. client가 전혀 모르게 여러가지 꾸미기(decrator) 기능들을 첨가할 수 있다. 

프록시들이 체인 형식을 이루면서 자신들의 꾸미기를 적용하게 된다.

 


 

실제 적용 예시

중간에 프록시가 끼워지게 되었다.

public class OrderControllerInterfaceProxy implements OrderControllerV1 {

    private final OrderControllerV1 target;
    private final LogTrace logTrace;

    @Override
    public String request(String itemId) {

        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderController.request()");
            //target 호출
            String result = target.request(itemId);
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }

    @Override
    public String noLog() {
        return target.noLog();
    }
}

프록시가 클라이언트의 호출을 받아서 로그 로직을 첨부하고 실제 컨트롤러를 호출하게 된다.

실제 컨트롤러를 건드리지도 않고 클라이언트도 신경안 쓰는 좋은 상황이다.

@Configuration
public class InterfaceProxyConfig {

    @Bean
    public OrderControllerV1 orderController(LogTrace logTrace) {
        OrderControllerV1Impl controllerImpl = new OrderControllerV1Impl(orderService(logTrace));
        return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
    }

    @Bean
    public OrderServiceV1 orderService(LogTrace logTrace) {
        OrderServiceV1Impl serviceImpl = new OrderServiceV1Impl(orderRepository(logTrace));
        return new OrderServiceInterfaceProxy(serviceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
        OrderRepositoryV1Impl repositoryImpl = new OrderRepositoryV1Impl();
        return new OrderRepositoryInterfaceProxy(repositoryImpl, logTrace);
    }

}

@Configuration에서 수동으로 빈주입 설정을하여 모든 layer에서 프록시를 호출하도록 설정해줄 수 있다.

실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리를 하지 않는다. 개발자가 직접 생성한 객체라 그렇다.

현재는 인터페이스를 통한 프록시 생성을 했지만 인터페이스가 없어도 상속을 통해 프록시를 생성할 수 있다.

 

728x90