프록시 팩토리

바그다드·2023년 8월 30일
0

지난 포스팅까지 해서 JDK 동적 프록시와 CGLIB을 활용한 동적 프록시 생성 방법에 대해서 알아보았다.
지난 포스팅에서 드러난 문제가 몇가지 있었는데,

  1. 인터페이스가 있는 경우에는 JDK 동적 프록시를, 구현 클래스만 있는 경우에는 CGLIB을 사용해야 한다는 점
  2. 두가지 경우에 같은 부가 기능을 사용한다면 InvocationHandler와 MethodInterceptor를 각각 만들어야 하나 하는 문제
  3. 같은 부가기능이 아니더라도 InvocationHandler와 MethodInterceptor를 각각 만들어 관리를 해야하는 문제

그런데 JDK 동적 프록시와 CGLIB은 유사한 형태를 띄고 있고, 그 기능도 유사하다.
스프링은 이런 유사한 기술들에 대해 편리하게 사용 가능하도록 추상화된 기술을 제공한다.
그리고 그 기술이 ProxyFactory이다.

프록시 팩토리


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

  • 만약 인터페이스가 있는 로직과, 구체 클래스만 있는 로직에서 동일한 부가 로직을 사용한다면?

스프링에서는 이런 문제를 해결하기 위해 Advice라는 것을 지원한다.
결과적으로 InvocationHandler나 MethodInterceptor가 Advice를 호출하게 되기 때문에, 개발자는 두 경우를 따로 신경쓸 필요 없이 Advice를 만들면 된다.
프록시 팩토리를 사용하면 내부에서 Advice 를 호출하는 전용 InvocationHandler, MethodInterceptor를 사용하기 때문이다.

  • 특정 상황에만 프록시 로직을 적용하는 기능을 공통으로 제공하려면?
    스프링에서 제공하는 Pointcut을 이용하면 이 문제를 해결할 수 있다.

그럼 코드로 확인해보자.

프록시 팩토리 예제

1. Advice 생성

Advice는 프록시에 적용하는 부가 기능으로 InvocationHandler와 MethodInterceptor와 유사하며 이 둘을 추상화한 개념이다. 따라서 프록시 팩토리에서는 둘 대신에 Advice를 사용하면 된다.

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

@Slf4j
public class TimeAdvice implements MethodInterceptor {

    // 타겟의 정보는 MethodInvocation안에 이미 들어가 있음
    // 프록시 팩토리를 생성하는 시점에 타겟 정보를 넘겨야함
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

//        Object result = method.invoke(target, args);
        Object result = invocation.proceed();

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        log.info("TimeProxy종료 resultTime={}",resultTime);
        return result;
    }
}
  • Advice도 MethodInterceptor를 구현하는데, 이 MethodInterceptor는 CGLIB과 달리 'org.aopalliance.intercept'에 있는 MethodInterceptor를 말한다.
  • invocation.proceed()
    target클래스를 호출하고, 결과를 반환한다.
  • MethodInvocation invocation
    프록시 팩토리를 생성할 때 target 정보를 파라미터로 넘겨 받아 가지고 있다. 이후의 코드에서 확인하자.

2. 테스트 코드 생성

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceImpl target = new ServiceImpl();
        // 프록시 팩토리를 생성하는 시점에 타겟을 인수로 넘김
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        log.info("targetClass={}",target.getClass());
        log.info("proxyClass={}",proxy.getClass());

        proxy.save();

        // AopUtils는 프록시 팩토리를 이용해 프록시를 생성했을 경우에만 사용 가능
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
  • new ProxyFactory(target)
    프록시 팩토리를 생성할 때 인터페이스가 있다면 JDK 동적 프록시를, 없다면 CGLIB을 통래 프록시를 생성해준다.
    여기서는 인터페이스가 있기 때문에 JDK 동적 프록시를 사용한다.
  • proxyFactory.addAdvice(프록시 로직)
    프록시 팩토리에서 사용할 프록시 로직을 설정한다.
    InvocationHandler나 MethodInterceptor와 유사하다.
  • proxyFactory.getProxy()
    프록시 객체를 생성하고 Object타입으로 반환한다.
    따라서 적절하게 캐스팅 해주자.
  • AopUtils.isAopProxy(proxy) : 프록시 팩토리를 통해서 생성되었다면 참
    AopUtils.isJdkDynamicProxy(proxy) : 프록시 팩토리를 통해서 생성되었고, JDK 동적 프록시일 경우에만 참
    AopUtils.isCglibProxy(proxy) : 프록시 팩토리를 통해 생성되었고, CGLIB 동적 프록시일 경우에만 참

테스트 결과를 확인해보자.

  1. 인터페이스의 경우

    검증 결과도 참으로 나오고, 로그도 AOP 동적 프록시가 찍히는 것을 확인할 수 있다.

  2. 구현 클래스의 경우

  • 인터페이스와 유사해서 코드는 생략한다.
  1. 인터페이스이지만 CGLIB을 사용하고 싶은 경우
    @Test
    @DisplayName("ProxyTargetClass를 사용하면 인터페이스가 있어도 CGLIB을 사용하고, 구현 클래스 기반 프록시 사용")
    void proxyTargetClass() {
        ServiceImpl 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();

        // AopUtils는 프록시 팩토리를 이용해 프록시를 생성했을 경우에만 사용 가능
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
        assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
    }
  • proxyFactory.setProxyTargetClass(true)
    이 설정을 적용하면 인터페이스가 있더라도 CGLIB을 이용해 프록시를 생성한다.

이처럼 프록시 팩토리를 활용하면 JDK 동적 프록시나 CGLIB에 관계 없이 동적 프록시를 편리하게 생성할 수 있다.
스프링에서는 Advice로 추상화를 제공하고 있는데, 프록시 팩토리 내부에서 JDK 동적 프록시의 경우 InvocationHandler를, CGLIB의 경우 MethodInterceptor를 호출하도록 개발하였다.
덕분에 우리는 Advice라는 추상화를 활용해 프록시 로직을 편하게 적용할 수 있게 되었다.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글