• 프록시 패턴을 사용할 때 프록시 클래스를 직접 생성한다면 엄청나게 많은 클래스를 개발자가 직접 생성해야 할 것이다
  • 동적 프록시 기술을 사용한다면 개발자가 직접 프록시 클래스를 생성할 필요 없이 런타임시 알아서 생성해서 사용할 수 있다

1. JDK Dynamic 프록시

  • 인터페이스 기반 프록시
  • 리플렉션으로 프록시 객체를 생성
public interface AInterface {
   String call();
}

@Slf4j
public class AImpl implements AInterface {
   @Override
   public String call() {
      log.info("A 호출");
      return "a"; 
   }
}
  • InvocationHandler 인터페이스 : 프록시에 적용할 공통 로직을 구현
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
  • Object proxy : 프록시 자신
  • Method method : 호출한 메서드
  • Object[] args : 메서드 호출시 전달되는 인수
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
   private final Object target;

   public TimeInvocationHandler(Object target) {
      this.target = target;
   }  

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      long startTime = System.currentTimeMillis();

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

      long endTime = System.currentTimeMillis();
      long resultTime = endTime - startTime;
      log.info("TimeProxy 종료 resultTime={}", resultTime); 
      
      return result;
   }
}
  • Object target : 프록시가 호출할 대상
  • invoke(target, args) : target 인스턴스의 메서드 실행
  • TimeInvocationHandler(Object target) : 생성자로 타겟을 넘겨서 프록시 생성

  • 프록시 사용
    • Proxy.newProxyInstance() : 동적 프록시 생성
    • 클래스로더, 인터페이스, 핸들러를 파라미터로 입력
public class Main {
   public static void main(String[] args) {
      Ainterface target = new AImpl();
      TimeInvocationHandler handler = new TimeInvocationHandler(target);
      AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), 
            new Class[] {AInterface.class}, handler);
      proxy.call();
   }
}

  • 동적으로 생성된 프록시를 통해 입력된 핸들러를 실행
  • 핸들러는 공통로직을 수행하고 핵심로직을 invoke()로 실행

2. CGLIB 프록시 (기본)

  • 구체 클래스 기반 프록시 (인터페이스가 없어도 프록시를 생성할 수 있다)
  • CGLIB (Code Generator LIBrary) : 바이트코드를 조작해서 동적으로 클래스(프록시)를 생성하는 라이브러리
  • 스프링부트 2.0부터 CGLIB가 default
@Slf4j
public class AService {
   public String call() {
      log.info("A 호출");
      return "a"; 
   }
}
  • MethodInterceptor 인터페이스 : 프록시에 적용할 공통 로직을 구현
    • org.springframework.cglib.proxy.MethodInterceptor
public interface MethodInterceptor {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
  • Object ojb : 프록시가 적용될 객체
  • Method method : 호출한 메서드
  • Object[] args : 메서드 호출시 전달되는 인수
  • MethodProxy proxy : 메서드 호출에 사용
import org.springframework.cglib.proxy.MethodInterceptor;

@Slf4j
public class TimeInvocationHandler implements MethodInterceptor {
   private final Object target;

   public TimeInvocationHandler(Object target) {
      this.target = target;
   }  

   @Override
   public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
      long startTime = System.currentTimeMillis();

      Object result = proxy.invoke(target, args);

      long endTime = System.currentTimeMillis();
      long resultTime = endTime - startTime;
      log.info("TimeProxy 종료 resultTime={}", resultTime); 
      
      return result;
   }
}
  • Object target : 프록시가 호출할 대상
  • invoke(target, args) : target 인스턴스의 메서드 실행
  • TimeInvocationHandler(Object target) : 생성자로 타겟을 넘겨서 프록시 생성

  • 프록시 사용
    • Enhancer : CGLIB에서 프록시를 생성하기 위한 클래스
    • create() : 동적프록시 생성
public class Main {
   public static void main(String[] args) {
      AService target = new AService();

      Enhancer enhancer = new Enhancer();
      enhancer.setSuperclass(ConcreteService.class);
      enhancer.setCallback(new TimeMethodInterceptor(target));

      AService proxy = (AService) enhancer.create();
      proxy.call();
   }
}

프록시 팩토리 (ProxyFactory)

  • 스프링에서는 특정 조건에 맞을 시 해당 조건에 맞는 프록시 로직을 적용할 수 있도록 추상화 되어있다
  • ProxyFactory를 사용해 동적 프록시를 통합해서 편리하게 만들 수 있다
    • JDK 동적 프록시 : InvocationHandler 사용
    • CGLIB 프록시 : MethodInterceptor 사용

Advice

  • 공통 기능이 들어있는 로직
  • Advice로 공통 로직을 구현하면 InvocationHandler이나 MethodInterceptor가 호출되어 공통 로직을 수행한다

  • 공통로직 구현
    • MethodInterceptor 구현 org.aopalliance.intercept.MethodInterceptor
    • MethodInvocation : target의 정보가 모두 포함되어 있는 클래스
    • proceed() : target 인스턴스의 메서드 실행
import org.aopalliance.intercept.MethodInterceptor;

@Slf4j
public class TimeAdvice implements MethodInterceptor {
   @Override
   public Object invoke(MethodInvocation invocation) throws Throwable {
      long startTime = System.currentTimeMillis();

      Object result = invocation.proceed();

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

      return result;
   }
}
  • 프록시 사용
    • ProxyFactory(target) : 생성자로 타겟을 넘겨 프록시팩토리 생성
    • addAdvice() : 구현한 공통로직 설정
    • getProxy() : 팩토리에서 생성된 프록시객체 반환
public class Main {
   public static void main(String[] args) {
      AService target = new AService();

      ProxyFactory proxyFactory = new ProxyFactory(target);

      // Advice만 적용
      // proxyFactory.addAdvice(new TimeAdvice());

      // Advisor 적용 (Advice + Pointcut)
      AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
      pointcut.setExpression("execution(* hello.proxy.aop..*(..))");
      proxyFactory.addAdvisor(new DefaultPointcutAdvisor(pointcut, new TimeAdvice()));

      AService proxy = (AService) proxyFactory.getProxy();
      proxy.call();
   }
}

스프링에서 일반적으로 프록시를 사용할 때는 @Aspect를 사용한다
Spring AOP 링크