프록시 팩토리

현시기얌·2021년 11월 23일
0

AOP

목록 보기
4/19

Question) 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야 할까?

스프링은 유사한 구체적인 기술들이 있을 때 그것들을 통합해서 일관성 있게 접근할 수 있고 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 라는 기능을 제공한다.
이전에는 상황에 따라서 JDK 동적 프록시를 사용하거나 CGLIB를 사용해야 했다면 이제는 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고 구체 클래스만 있다면 CGLIB를 사용한다.
그리고 이 설정을 변경할 수 도 있다.

프록시 팩토리

클라이언트에서 프록시 요청이 오면 프록시 팩토리에서 인터페이스가 있는 것을 확인하면 JDK 동적 프록시를 선택하고 구체 클래스면 CGLIB를 생성한다.

Question) 두 기술을 함께 사용할 떄 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?

스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice라는 새로운 개념을 도입했다.
개발자는 InvocationHandler나 MethodInterceptor를 신경쓰지 않고, Advice만 만들면 된다.
결과적으로 InvocationHandler나 MethodInterceptor는 Advice를 호출하게 된다.
프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler, MethodInterceptor를 내부에서 사용한다.

Advice

만약 클라이언트가 JDK 프록시를 호출하면 adviceInvocationHandler가 호출되어 Advice를 호출한다.
마찬가지로 클라이언트가 CGLIB를 호출하면 adviceMethodInterceptor가 호출되어 Advice를 호출한다.

Question) 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?

스프링은 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.

Advice 만들기

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

Advice의 기본적인 방법은 아래의 인터페이스를 구현하는것이다.

public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation invocation) throws Throwable;
}
  • MethodInvocation invocation : 내부에는 다음 메소드를 호출하는 방법, 현재 프로깃 객체 인스턴스, args, 메소드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
  • CGLIB의 MethodInterceptor와 이름이 같으므로 패키지 이름에 주의해야 한다.
  • MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice 인터페이스를 상속한다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeProxy 실행");
        final long startTime = System.currentTimeMillis();
        
        final Object result = invocation.proceed();

        final long endTime = System.currentTimeMillis();
        final long resultTime = endTime - startTime;
        log.info("TimeProxy 종료 resultTime = {}", resultTime);

        return result;
    }
}
  • Object result = invocation.proceed()
    • invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
    • target 클래스의 정보는 MethodInvocation invocation 안에 모두 포함되어 있다.
    • 그 이유는 프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문이다.

테스트 코드

@Slf4j
public class ProxyFactoryTest {
    
    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy()  {
        //given
        final ServiceImpl target = new ServiceImpl();
        final ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());
        final ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
        log.info("targetClass = {}", target.getClass());
        log.info("proxyClass = {}", proxy.getClass());
        //when
        proxy.save();
        //then
        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
}

실행 결과

targetClass = class hello.proxy.common.service.ServiceImpl
proxyClass = class com.sun.proxy.$Proxy10
TimeProxy 실행
find 호출
TimeProxy 종료 resultTime = 0

  • new ProxyFactory(target) : 프록시 팩토리를 생성할 때 생성자에 프록시의 호출 대상을 함께 넘겨준다.
    프록시팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다.
    만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다.
    여기서는 target이 new ServiceImpl()의 인스턴스이기 때문에 ServiceInterface가 있다.
    따라서 인터페이스를 기반으로 JDK 동적 프록시를 생성한다.
  • proxyFactory.addAdvice(new TimeAdvice()) : 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 로직을 설정한다.
    JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념과 유사하다.
    이렇게 프록시가 제공하는 부가 기능 로직을 Advice라 한다.
    사실 proxyFactory.addAdvice(new TimeAdvice()) 이렇게 어드바이스를 바로 적용하는 것은 단순히 편의 메소드일 뿐이다.
    실제로는 메소드 내부에서 DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())와 같이 어드바이저가 생성되는 것이다.
  • proxyFacotry.getProxy() : 프록시 객체를 생성하고 그 결과를 반환 받는다.

프록시 팩토리를 통한 프록시 적용 확인

AopUtils.isAopProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나 CGLIB 모두 참이다.
AopUtils.isJdkDynamicProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참이다.
AopUtils.isCglibProxy(proxy) : 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 참인다.

ProxtTargetClass 옵션

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

프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다.
그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어 준다.

실행 결과

targetClass = class hello.proxy.common.service.ServiceImpl
proxyClass = class hello.proxy.common.service.ServiceImpl $$ EnhancerBySpringCGLIB$$580e91
TimeProxy 실행
save 호출
TimeProxy 종료 resultTime = 44

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

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

정리

  • 프록시 팩토리의 서비스 추상화 덕분에 구체적인 CGLIB, JDK 동적 프록시 기술에 의존하지 않고, 매우 편리하게 동적 프록시를 생성할 수 있다.
  • 프록시의 부가 기능 로직도 특정 기술에 종속적이지 않게 Advice 하나로 편리하게 사용할 수 있었다.
    이것은 프록시 팩토리가 내부에서 JDK 동적 프록시 인 경우 InvocationHandler가 Advice를 호출하게 개발해두고,
    CGLIB인 경우 MethodInterceptor가 Advice를 호출하도록 기능을 개발해두었기 때문이다.

cf) 참고

스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체클래스를 기반으로 프록시를 생성한다.

profile
현시깁니다

0개의 댓글