본문 바로가기
Spring/Advanced

스프링이 지원하는 프록시

by JHyun0302 2023. 8. 17.
728x90

프록시 팩토리

 

 

문제점 : 인터페이스가 있는 경우에는 JDK 동적 프록시가 제공하는 (InvocationHandler), 인터페이스가 없는 경우에는 CGLIB가 제공하는 (MethodInterceptor)각각 만들어서 관리해야할까?

 

 

해결 방안 : 동적 프록시를 통합해서 만들어주는 ( ProxyFactory )

 

 

 

ProxyFactory - 의존 관계

 

 

 

 

 

 

ProxyFactory - 전체 흐름

 

  • 스프링은 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다.
  • 결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
  • 프록시 팩토리는 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.

 

 

 

 

 

Advice 도입

 

 

 

 

 

 

 

Advice 도입 - 전체 흐름

 

  • 특정 로직을 체크해서 프록시를 적용할지 말지 결정하는 Pointcut 개념 도입

 

 

 

 

 

 

 


프록시 팩토리 - 예제

 

 

 

 

MethodInterceptor - 스프링이 제공하는 코드

public interface MethodInterceptor extends Interceptor {
	Object invoke(MethodInvocation invocation) throws Throwable;
}

 

  • Advice 를 만드는 방법 - MethodInterceptor 구현

 

 

 

 

 

 

 

TimeAdvice

@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();
        
        Object result = invocation.proceed();
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime; 
        log.info("TimeProxy 종료 resultTime={}ms", resultTime); 
        return result;
    }
}

 

  • Object result = invocation.proceed()
             invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
             ProxyFactory로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문에 target 정보 X.

 

 

 

 

 

ProxyFactoryTest

//JDK 동적 프록시 적용 Test
//CGLIB 적용 Test

//인터페이스 있어도 CGLIB 강제 적용

@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl(); 
    ProxyFactory proxyFactory = new ProxyFactory(target); 
    proxyFactory.setProxyTargetClass(true); //중요
    proxyFactory.addAdvice(new TimeAdvice());
    
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    log.info("targetClass={}", target.getClass());
    log.info("proxyClass={}", proxy.getClass());
    proxy.save();

    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}

 

  • proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제 CGLIB 적용시킨다.

 

 

 

 

 

실행 결과

//JDK 동적 프록시 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl 
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy13
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms

//CGLIB 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ConcreteService 
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$103821ba
TimeAdvice - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms

//인터페이스 있어도 강제로 CGLIB 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl 
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ServiceImpl$$EnhancerBySpringCGLIB$$2bbf51ab
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms

 

 

 

★ 프록시 팩토리의 기술 선택 방법

  • 대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
  • 대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
  • proxyTargetClass=true : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음

 

 

 

★ 정리 

  •  ProxyFactory 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice 를 호출하도록 개발해두고,
  • CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다.

 

 

 

 

 

 


포인트컷, 어드바이스, 어드바이저 - 소개

 

 

 

 

  • 포인트컷( Pointcut ) : 부가 기능을 어디에 적용할지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다.
                                        이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다.
  • 어드바이스( Advice ) : 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다.
  • 어드바이저( Advisor ) : 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 포인트컷1 + 어드바이스1이다.

 

 

 

 

 

 

※ 역할과 책임

  • 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
  • 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
  • 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.

 

 

 

 

 

 

 

 

 

Advisor 전체 구조

 

 

 

 

 


Advisor - 예제

 

 

 

 

 

 

 

 

 

 

 

 

Pointcut 관련 인터페이스 - 스프링 제공

public interface Pointcut {
    ClassFilter getClassFilter();
    MethodMatcher getMethodMatcher();
}

public interface ClassFilter {
    boolean matches(Class<?> clazz);
}

public interface MethodMatcher {
    boolean matches(Method method, Class<?> targetClass);
    //..
}

 

 

 

 

 

AdvisorTest

@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
    ServiceImpl target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedNames("save");
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
    proxyFactory.addAdvisor(advisor);
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    proxy.save();
    proxy.find();
}

 

  • NameMatchMethodPointcut 을 생성하고 setMappedNames(...) 으로 메서드 이름을 지정하면 포인트컷이 완성된다.

 

 

 

save() 호출

 

 

 

 

 

find() 호출

 

 

 

스프링이 제공하는 포인트컷

  1. NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다.
                                                  ex) *xxx* 허용
  2. JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
  3. TruePointcut : 항상 참을 반환한다.
  4. AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
  5. AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.

 

 

 

 

 

 

 

 


여러 어드바이저 함께 적용

 

 

MultiAdvisorTest

@Test
@DisplayName("여러 프록시") void multiAdvisorTest1() {
    //client -> proxy2(advisor2) -> proxy1(advisor1) -> target
    
    //프록시1 생성
    ServiceInterface target = new ServiceImpl(); 
    ProxyFactory proxyFactory1 = new ProxyFactory(target); 
    DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    proxyFactory1.addAdvisor(advisor1);
    ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
    
    //프록시2 생성, target -> proxy1 입력
    ProxyFactory proxyFactory2 = new ProxyFactory(proxy1); 
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2()); 
    proxyFactory2.addAdvisor(advisor2);
    ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy(); 
    
    //실행
    proxy2.save();
}

 

 

 

 

 

 

MultiAdvisor - 런타임 동작

 

 

 

 

문제 : 프록시 N개 생성해야 한다. 즉, Advisor (N개) + Proxy (N개) 만들어야한다.

 

 

해결 방안 : 하나의 프록시, 여러 어드바이저

 

 

 

 

 

 

 

MultiAdvisorTest

@Test
@DisplayName("하나의 프록시, 여러 어드바이저") void multiAdvisorTest2() {
    //proxy -> advisor2 -> advisor1 -> target
    
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
    DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory1 = new ProxyFactory(target);
    proxyFactory1.addAdvisor(advisor2);
    proxyFactory1.addAdvisor(advisor1);
    ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
   
    //실행
    proxy.save();
}

 

 

 

 

 

1개 Proxy, 여러개 Advisor

 

 

 

중요

→ 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링 AOP는 target 마다 하나의 프록시만 생성한다.

 

 

 

 

 

 

 

 


ProxyFactory - 적용

 

 

LogTraceAdvice - 인터페이스가 있는 경우

@Slf4j
public class LogTraceAdvice implements MethodInterceptor {
    private final LogTrace logTrace;
        public LogTraceAdvice(LogTrace logTrace) {
        this.logTrace = logTrace;
    } 
    
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;
        try {
            Method method = invocation.getMethod();
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);
        
            //로직 호출
            Object result = invocation.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    } 
}

 

 

 

 

ProxyFactoryConfig

@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
     @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }

    // OrderServiceV1
    
    // OrderRepositoryV1

    private Advisor getAdvisor(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

 

 

 

애플리케이션 로딩 로그 - JDK 동적 프록시 적용

ProxyFactory proxy=class com.sun.proxy.$Proxy50,
target=class ...v1.OrderRepositoryV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy52,
target=class ...v1.OrderServiceV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy53,
target=class ...v1.OrderControllerV1Impl

 

 

 

 

 

 

ProxyFactoryConfig
 
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {

    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(),
        orderController.getClass());
        return proxy;
    } 
    
    // OrderServiceV1

    // OrderRepositoryV1
    
    private Advisor getAdvisor(LogTrace logTrace) {
        //pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");
        //advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);
        //advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}

 

 

 

애플리케이션 로딩 로그

ProxyFactory proxy=class hello.proxy.app.v2.OrderRepositoryV2$
$EnhancerBySpringCGLIB$$594e4e8, target=class
hello.proxy.app.v2.OrderRepositoryV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderServiceV2$
$EnhancerBySpringCGLIB$$59e5130b, target=class
hello.proxy.app.v2.OrderServiceV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderControllerV2$
$EnhancerBySpringCGLIB$$79c0b9e, target=class
hello.proxy.app.v2.OrderControllerV2

 

 

 

 

정리 : 어드바이저, 어드바이스, 포인트컷 덕분에 어떤 부가 기능어디에 적용할 지 명확해졌다.

 

 

 

 

 

 

 


 

문제 1 : 너무 많은 설정

  • ProxyFactoryConfig 같은 설정 파일이 너무 많다.

 

문제 2 : 컴포넌트 스캔

  • 컴포넌트 스캔을 사용하는 경우 프록시 적용이 불가능하다.
  • 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.

 

 

 

 

해결 방안 : 빈 후처리기

 

 

반응형

'Spring > Advanced' 카테고리의 다른 글

@Aspect AOP  (0) 2023.08.21
빈 후처리기  (0) 2023.08.17
동적 프록시 기술  (0) 2023.08.16
프록시 패턴과 데코레이터 패턴  (0) 2023.08.14
템플릿 메서드 패턴과 콜백 패턴  (0) 2023.08.13