본문 바로가기
Spring/spring AOP

스프링 코어2 - 구체 클래스 기반 프록시

by 킹차니 2022. 1. 8.

전 포스트에서는 인터페이스에 의존하는 클라이언트를 대상으로 프록시를 적용하였다. 그런데 아래와 같이 인터페이스가 없는 경우에는 어떻게 프록시를 적용할 수 있을까?

@Slf4j
public class ConcreteLogic {

    public String operation(){
        log.info("Concrete Logic 실행");
        return "data";
    }
}

이를 사용하는 Client는 아래와 같다.

public class ConcreteClient {

    private ConcreteLogic concreteLogic;

    public ConcreteClient(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

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

이와 같은 상황에서 자바의 다형성을 사용하여 프록시를 도입할 수 있다.

자바의 다형성은 인터페이스를 구현하든, 클래스를 상속하든 상위 타입만 맞다면 다형성이 적용된다. 즉 굳이 인터페이스를 만들지 않아도 프록시를 적용할 수 있다는 것이다. 하여 이번에는 인터페이스가 아닌 클래스를 기반으로 상속을 받아 프록시를 만들어 보자.

 

로직 실행 시간을 측정을 하는 프록시를 만들 것이다. 위에서 상속을 사용하여 프록시를 가능하게 할 것이기 때문에 프록시는 ConcreteLogic을 extends하면 된다.

@Slf4j
public class TimeProxy extends ConcreteLogic{

    private ConcreteLogic target;

    public TimeProxy(ConcreteLogic target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("TimeDecorator 실행");
        long startTime = System.currentTimeMillis();
        String result = target.operation(); // ConcreteLogic의 operation 호출
        long endTime = System.currentTimeMillis();
        log.info("TimeDecorator 종료, result time = {}", (endTime - startTime));
        return result;
    }
}

이제 클라이언트는 ConcreteLogic은 물론 TimeProxy도 주입받을 수 있게 되었다. 이를 테스트해보자.

@Test
void yesProxy(){ // Clien --> Proxy --> ConcreteLogic 의 의존관계를 가진다.
    ConcreteClient concreteClient = new ConcreteClient(new TimeProxy(new ConcreteLogic()));
    concreteClient.execute();
}


실행결과:

21:14:06.221 [Test worker] INFO hello.proxy.pureproxy.concreteproxy.code.TimeProxy - TimeDecorator 실행
21:14:06.222 [Test worker] INFO hello.proxy.pureproxy.concreteproxy.code.ConcreteLogic - Concrete Logic 실행
21:14:06.223 [Test worker] INFO hello.proxy.pureproxy.concreteproxy.code.TimeProxy - TimeDecorator 종료, result time = 1

잘 적용된 것을 알 수 있다.

 

이제 위에서 본 상속을 이용한 프록시를 V2 App에 적용해보자.

 

ControllerProxy

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

 

ServiceProxy

public class OrderServiceConcreteProxy extends OrderServiceV2 {

    private final OrderServiceV2 target;
    private final LogTrace logTrace;

    public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {
        super(null);//어째 영 불편~
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public void orderItem(String itemId) {
        TraceStatus status = null;
        try{
            status = logTrace.begin("OrderService.orderItem()");
            target.orderItem(itemId);//target 호출
            logTrace.end(status);
        }catch(Exception e){
            logTrace.exception(status, e);
            throw e;
        }
    }
}

 

RepositoryProxy

public class OrderRepositoryConcreteProxy extends OrderRepositoryV2 {

    private final OrderRepositoryV2 target;
    private final LogTrace logTrace;

    public OrderRepositoryConcreteProxy(OrderRepositoryV2 target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public void save(String itemId) {
        TraceStatus status = null;
        try{
            status = logTrace.begin("OrderRepository.request()");
            target.save(itemId);//target 호출
            logTrace.end(status);
        }catch(Exception e){
            logTrace.exception(status, e);
            throw e;
        }
    }
}

 

이제 의존 주입을 해주자

package hello.proxy.config.v1_proxy;

import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderControllerConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderRepositoryConcreteProxy;
import hello.proxy.config.v1_proxy.concrete_proxy.OrderServiceConcreteProxy;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConcreteProxyConfig {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace){
        OrderControllerV2 orderControllerImpl = new OrderControllerV2(orderServiceV2(logTrace));
        return new OrderControllerConcreteProxy(orderControllerImpl, logTrace);
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace){
        OrderServiceV2 orderServiceImpl = new OrderServiceV2(orderRepositoryV2(logTrace));
        return new OrderServiceConcreteProxy(orderServiceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace){
        OrderRepositoryV2 repositoryImpl = new OrderRepositoryV2();
        return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);
    }

}

 

그리고 아래와 같이 메인 클래스에 @Import를 수정해준다.

@Import(ConcreteProxyConfig.class)
//@Import(InterfaceProxyConfig.class)
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
   public static void main(String[] args) {
      SpringApplication.run(ProxyApplication.class, args);
   }

   @Bean
   public LogTrace logTrace(){
      return new ThreadLocalLogTrace();
   }
}

 

이제 어플리케이션을 실행한 뒤 요청을 해보았다.

 

실행결과 로그가 잘 출력되는 것을 볼 수 있다.

2022-01-08 21:40:24.744  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] OrderController.request()
2022-01-08 21:40:24.747  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] |-->OrderService.orderItem()
2022-01-08 21:40:24.747  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] |   |-->OrderRepository.request()
2022-01-08 21:40:25.751  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] |   |<--OrderRepository.request() time=1004ms
2022-01-08 21:40:25.752  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] |<--OrderService.orderItem() time=1005ms
2022-01-08 21:40:25.752  INFO 4966 --- [nio-8080-exec-1] h.p.trace.logtrace.ThreadLocalLogTrace   : [76541b54] OrderController.request() time=1008ms

 

 

지금까지 프록시를 적용하여 기존 코드를 변경하지 않고, 로그 추적기를 도입할 수 있었다. 하지만 각 Controller, Service, Repository 마다 프록시 클래스들이 생성되어 너무 많은 프록시 코드들이 존재하게 되었다. 즉 대상 클래스가 100개라면 100개의 프록시 클래스를 만들어야 한다! 하여 동적 프록시 기술을 사용하여 이와 같은 문제를 해결할 수 있다.

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.