FireDrago

[Spring] 스프링이 지원하는 프록시 본문

프로그래밍/Spring

[Spring] 스프링이 지원하는 프록시

화이용 2024. 5. 15. 14:30

프록시 팩토리

스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리( ProxyFactory )라는 기능을 제공한다.

프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB를 사용한다.

그리고 이 설정을 변경할 수도 있다. 이전에는 상황에 따라서 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면, 이제는 이 프록시 팩토리 하나로편리하게 동적 프록시를 생성할 수 있다.

 

Advice 도입

프록시 팩토리를 사용하면 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서사용한다.

Advice는 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor  둘을 개념적으로 추상화 한 것이다. 프록시 팩토리를 사용하면 둘 대신에 Advice 를 사용하면 된다.

 

@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();
        log.info("TimeProxy 종료 resultTime={}ms", endTime - startTime);
        return result;
    }
}

Advice를 만들때는 MethodInterceptor 인터페이스를 구현한다.

invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.

target 클래스의 정보는 (MethodInvocation invocation) 안에 모두 포함되어있다.

프록시를 생성하는 단계에서 프록시 팩토리에 target 정보를 미리 전달하기때문이다.

 

@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
    ServiceInterface target = new ServiceImpl();
    // 구체클래스냐 인터페이스 구현체냐에 따라, JDK동적 프록시, CGLib 사용
    ProxyFactory proxyFactory = new ProxyFactory(target);
    // advice 설정
    proxyFactory.addAdvice(new TimeAdvice());
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

    log.info("targetClass={}", target.getClass());
    log.info("proxyClass={}", proxy.getClass());

    // 프록시를 통한 실제 객체 메서드 호출 (advice 함께 실행)
    proxy.save();

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

new ProxyFactory(target) : 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다. 
프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다. 만약 이 인스턴스에 인터페이스가 있다면
JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시
를 생성한다. 여기서는 target 이 new ServiceImpl() 의 인스턴스이기 때문에 ServiceInterface 인
터페이스가 있다. 따라서 이 인터페이스를 기반으로 JDK 동적 프록시를 생성한다.

 

proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가기능 로직을 설정한다

 

proxyFactory.getProxy() : 프록시 객체를 생성하고 그 결과를 받는다

 

@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 없어도 CGLIB를 사용한다")
void proxyTargetClass() {
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new TimeAdvice());
    // 인터페이스가 있어도, 강제로 CGLIB 사용 , 스프링부트 AOP 기본값
    proxyFactory.setProxyTargetClass(true);
    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();
}

인터페이스가 있지만, CGLIB를 사용해서 클래스 기반으로 동적 프록시를 만들 수 도 있다.

프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도

강제로 CGLIB를 사용한다

 

포인트컷, 어드바이스, 어드바이저

포인트컷 : 어디에 부가기능을 적용할지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다.

어드바이스 : 프록시가 호출하는 부가기능이다.

어드바이저 : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것, (포인트컷1 + 어드바이스1)

 

포인트컷 설정하기

@Test
@DisplayName("스프링이 제공하는 포인트 컷")
void advisorTest3() {
    // 프록시 만들 실제 객체 
    ServiceImpl target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    // 메서드 이름으로 포인트컷 생성하기
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedName("save");
    // 포인트컷과 어드바이스로 어드바이저 생성하기
    DefaultPointcutAdvisor advisor =
            new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
    // 프록시 팩토리에 어드바이저 설정하기
    proxyFactory.addAdvisor(advisor);
    // 프록시 생성
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    // 프록시를 통한 로직 실행
    proxy.save();
    proxy.find();
}

 

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

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

NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다.

AnnotationMatchingPointcut : 애노테이션으로 매칭한다

AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.  예) "excution(* hello.proxy..*(..))"

 

 

여러 어드바이저 함께 적용

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

프록시 팩토리에 원하는 만큼 addAdvisor() 를 통해서 어드바이저를 등록하면 된다.

등록하는 순서대로 advisor 가 호출된다. 여기서는 advisor2 , advisor1 순서로 등록했다.

스프링은 프록시는 하나만 만들고, 하나의 프록시에 여러개의 어드바이저를 적용한다.