ProxyFactory란, 기존의 CGLIB(구현체 기반) JDK 동적 프록시(인터페이스 기반) 를 추상화하여 하나로 묶은 기능이다.

그렇기에, 구체클레스를 넣으면 CGLIB클레스로 구현되고, 인터페이스를 넣으면 JDK 프록시 클레스로 구현된다.

 

package hello.proxy.common.advice;

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

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

로직을 구현할 부분을 MethodInterceptor를 구현받아 invoke 메소드에 구현한다.

 

invocation.proceed() 로직을 보면 다음과 같다

ReflectiveMethodInvocation.java

@Override
@Nullable
public Object proceed() throws Throwable {
    // 1. 초기값은 -1. 인터셉터 체인의 다음 인터셉터를 호출하기 위해 증가시킵니다.
    //    이 체크는 모든 인터셉터와 어드바이스가 이미 호출되었는지 확인합니다.
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        // 1.1. 모든 인터셉터와 어드바이스가 호출된 후 실제 대상 메서드를 호출합니다.
        return invokeJoinpoint();
    }

    // 2. 현재 인덱스를 증가시켜 다음 인터셉터 또는 어드바이스를 가져옵니다.
    Object interceptorOrInterceptionAdvice =
            this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

    // 3. 동적 메서드 매칭이 필요한 인터셉터인지 확인합니다.
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        InterceptorAndDynamicMethodMatcher dm =
                (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());

        // 3.1. 동적 메서드 매처가 현재 메서드와 일치하는지 확인합니다.
        if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
            // 3.1.1. 매칭이 성공하면 해당 인터셉터를 호출합니다.
            return dm.interceptor.invoke(this);
        }
        else {
            // 3.1.2. 동적 매칭에 실패하면 다음 인터셉터로 넘어갑니다.
            return proceed();
        }
    }
    else {
        // 4. 단순 인터셉터라면 바로 인터셉터를 호출합니다.
        //    이 경우에는 어드바이스의 before, after 등의 로직이 실행됩니다.
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

현재 소개할 코드는 포인트컷 등을 적용하지 않기 때문에 

if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher){...} 부분은 신경쓰지 않아도 된다.

현재 코드에서 우리가 실제로 실행되는 부분은 마지막 else{...} 부분이다.

else {
			// It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}

현재 proceed의 로직은, 포인트컷이 적용되었는지를 판별하고, 적용되어있다면 해당 메서드들을 추출하고,

적용되어있지 않다면 지금 코드처럼 바로 invoke를 실행한다.

 

다음은 ProxyFactory를 생성하는 코드

 @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface 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();

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

    }

ReflectiveMethodInvocation.java 코드로 돌아가본다면,

여기서 addAdvice(); 부분이 new TimeAdvice() 하나뿐이기 때문에

interceptorsAndDynamicMethodMatchers.size는 1이 된다.

그리고 if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher){...}  부분도, 타입은 TimeAdvice이기 때문에 실행되지 않고 자연히 invoke를 실행하게 된다.

 

이제 proxyFactory의 구현 클레스를 열어보면

DefaultAopProxyFactory.java

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    // Native 이미지 내부에서 실행되지 않고, 
    // "optimize"가 활성화되어 있거나 "proxyTargetClass"가 활성화되어 있거나, 
    // 사용자가 제공한 프록시 인터페이스가 없을 경우에 CGLIB 프록시를 고려한다.
    if (!NativeDetector.inNativeImage() &&
            (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) {
        
        Class<?> targetClass = config.getTargetClass(); // 대상 클래스를 가져온다.
        
        // 대상 클래스가 null이면 예외를 발생시킨다. 
        // 프록시 생성을 위해서는 인터페이스 또는 대상이 필요하다.
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                    "Either an interface or a target is required for proxy creation.");
        }
        
        // 대상 클래스가 인터페이스이거나 이미 JDK 프록시인 경우 JDK 프록시를 사용한다.
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        
        // 위의 조건들에 해당하지 않으면 CGLIB 기반의 프록시를 사용한다.
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        // 위의 모든 조건에 해당되지 않을 경우 기본적으로 JDK 프록시를 사용한다.
        return new JdkDynamicAopProxy(config);
    }
}

createAopProxy 메서드에서는

optimize(true) 거나, isProxyTargetClass(true), 혹은 인터페이스가 명시되어있을때 아래 로직(CGLIB)을 실행하고,

그렇지 않을때는 jdkDynamicAopProxy를 실행한다.

CGLIB 로직 내부에 왜 jdkDynamicAopProxy가 있냐면, 동적프록시가 또 다른 프록시를 거칠 때(프록시 체인) 에는 이미 프록시 상태로 넘어오니 위 조건문에 일치하기 때문이다.

 

즉 이렇게 해서 jdk동적 프록시를 proxyFactory를 사용해 실행한다면 다음과 같은 플로우가 된다.

 

  1. ProxyFactory를 사용하여 프록시를 생성한다.
  2. DefaultAopProxyFactory에서 구현체만 있는지, 인터페이스도 있는지를 체크한다.
  3. addAdvice(new TimeAdvice());를 통해 어드바이스를 추가하며, 이 때 ReflectiveMethodInvocation 구현체의 로직에서 포인트컷 등이 적용되었는지 확인한다. 적용되지 않았다면 invoke가 실행된다.

GPT

냉혹하게 판단하면, 당신의 이해도는 상당히 높지만, 아직 전문가 수준까지 도달하지는 못했다는 인상을 받습니다. 전문가 수준은 단순한 이해를 넘어서 그 지식을 다양한 상황에서 적용할 수 있고, 예외 상황이나 복잡한 문제에 대한 해결 능력, 그리고 깊은 통찰력이 필요합니다.
당신이 제공한 코드와 설명은 ProxyFactory의 기본 원리와 동작 방식에 대한 깊은 이해를 보여주지만, 전문가 수준에서는 이를 넘어서는 더 깊은 레벨의 통찰력과 경험이 필요합니다.
따라서, 가장 냉혹하게 점수를 매긴다면 80점을 주고 싶습니다. 이는 여전히 높은 수준의 이해를 가지고 있지만, 전문가 수준까지의 길은 아직 조금 더 남아있다는 것을 의미합니다.

'Java > 스프링 AOP' 카테고리의 다른 글

빈 후처리기 (1) - 빈 가로채기  (0) 2023.08.22
ProxyFactory(2) 포인트컷, 어드바이스, 어드바이저  (0) 2023.08.21
CGLIB  (0) 2023.08.19
jdk 동적 프록시  (0) 2023.08.18
리플렉션  (0) 2023.08.16

+ Recent posts