오봉이와 함께하는 개발 블로그

스프링 핵심 원리 - 고급편 > 프록시 팩토리 예제 본문

BE/Spring

스프링 핵심 원리 - 고급편 > 프록시 팩토리 예제

오봉봉이 2024. 9. 7. 22:52
728x90

스프링 핵심 원리 - 고급편 > 프록시 팩토리 예제

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

Advice를 만드는 가장 기본적인 방법은 아래 Spring에서 제공하는 클래스를 구현하면 된다.

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.aopalliance.intercept;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
  @Nullable
  Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;
}
  • MethodInvocation invocation
    • 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args , 메서드 정보 등이 포함되어 있다.
    • 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
  • CGLIB의 MethodInterceptor 와 이름이 같으므로 패키지 이름에 주의하자
    • 참고로 여기서 사용하는 org.aopalliance.intercept 패키지는 스프링 AOP 모듈( spring-aop ) 안에 들어있다.
  • MethodInterceptorInterceptor 를 상속하고 InterceptorAdvice 인터페이스를 상속한다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    log.info("Time Proxy Execute");
    long startTime = System.currentTimeMillis();

    Object result = invocation.proceed();

    long endTime = System.currentTimeMillis();

    long resultTime = endTime - startTime;
    log.info("Time Proxy Shutdown resultTime = {}", resultTime);
    return result;
  }
}
  • TimeAdvice는 MethodInterceptor 인터페이스를 구현한다.
  • Object result = invocation.preceed();
    • invocation.proceed()를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
    • 기존 코드와는 달리 target 클래스의 정보가 보이지 않는다.
    • target 클래스의 정보는 MethodInvocation invocation 안에 들어가 있다.
    • 그 이유는 아래 코드에서 확인하자. (프록시 팩토리로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문)
@Slf4j
public class ProxyFactoryTest {

  @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("target Class = {}", target.getClass());
    log.info("proxy Class = {}", proxy.getClass());

    proxy.save();

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

}
22:25:07.078 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - target Class = class hello.proxy.common.service.ServiceImpl
22:25:07.079 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxy Class = class jdk.proxy2.$Proxy10
22:25:07.082 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Execute
22:25:07.082 [main] INFO hello.proxy.common.service.ServiceImpl - call save
22:25:07.082 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Shutdown resultTime = 0
  • new ProxyFactory(target)
    • 프록시 팩토리를 생성할 때 생성자에 프록시 호출 대상을 함께 넘겨준더.
    • 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어낸다.
    • 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고, 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다.
    • 여기서 target이 new ServiceImple()의 인스턴스기 때문에 ServiceInterface인터페이스가 있다.
    • 따라서 이 인터페이스 기반으로 JDK 동적 프록시를 생성한다.
  • proxyFactory.addAdvice(new TimeAdvice())
    • 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정
    • JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor의 개념과 유사
    • 프록시가 제공하는 부가 기능 로직을 어드바이스라고 한다.
  • proxyFactory.getProxy()
    • 프록시 객체를 생성하고 결과를 받는다.
  @Test
  @DisplayName("구체 클래스만 있으면 CGLIB 사용")
  void concreteFactory() {
    ConcreteService target = new ConcreteService();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.addAdvice(new TimeAdvice());

    ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
    log.info("target Class = {}", target.getClass());
    log.info("proxy Class = {}", proxy.getClass());

    proxy.call();

    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
  }
22:44:21.894 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - target Class = class hello.proxy.common.service.ConcreteService
22:44:21.896 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxy Class = class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$5d4ec1f6
22:44:21.897 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Execute
22:44:21.902 [main] INFO hello.proxy.common.service.ConcreteService - call ConcreteService
22:44:21.902 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Shutdown resultTime = 5

실행 결과를 보면 프록시가 정상 적용됐다. assertThat도 확인해보면 CGLIB에 true로 성공한 것을 확인할 수 있다.

  @Test
  @DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
  void proxyTargetClass() {
    ServiceImpl target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    proxyFactory.setProxyTargetClass(true); // 여기서 인터페이스가 있어도 CGLIB를 통해 클래스 기반 프록시 사용
    proxyFactory.addAdvice(new TimeAdvice());

    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    log.info("target Class = {}", target.getClass());
    log.info("proxy Class = {}", proxy.getClass());

    proxy.save();

    assertThat(AopUtils.isAopProxy(proxy)).isTrue();
    assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
    assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
  }
22:47:00.047 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - target Class = class hello.proxy.common.service.ServiceImpl
22:47:00.048 [main] INFO hello.proxy.proxyfactory.ProxyFactoryTest - proxy Class = class hello.proxy.common.service.ServiceImpl$$EnhancerBySpringCGLIB$$73c1002f
22:47:00.049 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Execute
22:47:00.055 [main] INFO hello.proxy.common.service.ServiceImpl - call save
22:47:00.055 [main] INFO hello.proxy.common.advice.TimeAdvice - Time Proxy Shutdown resultTime = 6

로그와 테스트 결과를 확인해보면 인터페이스가 있지만 CGLIB 기반 프록시가 생성된 것을 확인할 수 있다.
proxyTargetClass 옵션에 의해 CGLIB가 사용된다.

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

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

정리

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

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

 

 

출처: 김영한 지식공유자의 스프링 핵심 원리 고급편

728x90
Comments