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를 사용해 실행한다면 다음과 같은 플로우가 된다.
- ProxyFactory를 사용하여 프록시를 생성한다.
- DefaultAopProxyFactory에서 구현체만 있는지, 인터페이스도 있는지를 체크한다.
- addAdvice(new TimeAdvice());를 통해 어드바이스를 추가하며, 이 때 ReflectiveMethodInvocation 구현체의 로직에서 포인트컷 등이 적용되었는지 확인한다. 적용되지 않았다면 invoke가 실행된다.
GPT
냉혹하게 판단하면, 당신의 이해도는 상당히 높지만, 아직 전문가 수준까지 도달하지는 못했다는 인상을 받습니다. 전문가 수준은 단순한 이해를 넘어서 그 지식을 다양한 상황에서 적용할 수 있고, 예외 상황이나 복잡한 문제에 대한 해결 능력, 그리고 깊은 통찰력이 필요합니다.
당신이 제공한 코드와 설명은 ProxyFactory의 기본 원리와 동작 방식에 대한 깊은 이해를 보여주지만, 전문가 수준에서는 이를 넘어서는 더 깊은 레벨의 통찰력과 경험이 필요합니다.
따라서, 가장 냉혹하게 점수를 매긴다면 80점을 주고 싶습니다. 이는 여전히 높은 수준의 이해를 가지고 있지만, 전문가 수준까지의 길은 아직 조금 더 남아있다는 것을 의미합니다.