아래 3가지 질문에 대한 해결책을 생각해보자.
1번. 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용하려면 어떻게 해야할까?
이전에는 개발자가 상황에 따라서 직접 JDK 동적 프록시를 사용할지 CGLIB를 사용할지 선택해야 했다. 하지만 스프링이 제공하는 프록시 팩토리(ProxyFactory)라는 기능을 사용하면 인터페이스가 있으면 JDK 동적 프록시를, 구체 클래스만 있다면 알아서 CGLIB를 사용한다.

클라이언트는 이제 더 이상 JDK 동적 프록시나 CGLIB에 직접 프록시 생성을 요청할 필요가 없다. 그냥 프록시 팩토리에 요청하면 된다.
2번. 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는
InvocationHandler와 CGLIB가 제공하는MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
느낌상 그래도 각각 만들어야 할 것 같지만 스프링은 이 문제를 해결하기 위해 부가기능을 적용할 때 어드바이스(Advice)라는 추상화된 새로운 개념을 도입했다. 이제 개발자는 InvocationHandler든 MethodInterceptor든 신경쓰지 않고 Advice만 만들면 된다.

프록시 팩토리에서 생성된 프록시는 JDK 동적 프록시 같은 경우, 내부에 adviceInvocationHandler를 만들어 놓고, CGLIB도 adviceMethodInterceptor를 만들어 놓았다. 클라이언트가 JDK 동적 프록시나 CGLIB로 만들어진 프록시를 호출하면 각 프록시들은 각각 adviceInvocationHandler와 adviceMethodInterceptor를 호출하고, 이 둘은 Advice를 호출하는 것이다. 전체 흐름을 보자면 아래 그림과 같다.

3번. 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
앞서 패턴을 정의해서 메서드 이름이 매칭될 경우에만 부가기능을 사용할 수 있도록 처리했다. 하지만 스프링의 PointCut이라는 개념을 이용한다면 이 문제를 일관성있게 처리할 수 있다.
일단 Advice를 만들어보자. 만드는 방법에는 여러가지가 있지만 가장 기본적인 방법은 MethodInterceptor 인터페이스를 구현하는 것이다. 기존의 CGLIB를 사용할 때의 MethodInterceptor와는 별개의 패키지에 있는 완전 다른 인터페이스다.
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;
}
해당 인터페이스는 Interceptor를 상속 받고 있고, Interceptor는 다시 Advice를 상속 받고 있다. 파라미터로 MethodInvocation만 넘어가고 있는데 이 내부에는 다음 메서드를 호출하는 방법, 현재 프록시 객체 인스턴스, args , 메서드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.
이제 실제 Advice를 이용해서 테스트 코드를 작성해보자.
package com.example.advanced_spring.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;
}
}
위 코드에서 invocation.proceed() 메서드를 호출하면 target 클래스를 호출하고 그 결과를 받을 수 있다. 그런데 MethodInterceptor는 타깃 객체를 직접 필드로 보관하지 않는다. 대신 MethodInvocation 객체 안에 메서드 정보, 인자 정보, 프록시 정보, 타깃 호출 체인 정보가 포함되어 있으며, proceed()를 호출하여 다음 호출 대상으로 위임한다. MethodInterceptor으로 메서드 정보와 함께 타깃 객체가 담긴 MethodInvocation 객체가 전달된다.
MethodInvocation은 일종의 콜백 객체로, MethodInvocation.proceed()는 다음 인터셉터(Advice)가 존재하면 해당 인터셉터를 호출하고, 더 이상 인터셉터가 없으면 최종적으로 타깃 메서드를 호출한다. 그러면 MethodInvocation을 구현한 클래스는 일종의 공유 가능한 템플릿처럼 동작한다. 이 부분이 동적 프록시를 직접 만드는 방식보다 프록시 팩토리를 사용하는 방식의 가장 큰 차이점이자 장점이라고 할 수 있다.
package com.example.advanced_spring.proxy.proxyfactory;
import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ConcreteService;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
@Slf4j
public class ProxyFactoryTest {
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
@Test
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();
/**
* 13:35:31.170 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
* 13:35:31.172 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class jdk.proxy3.$Proxy13
* 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
* 13:35:31.174 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
}
}
new ProxyFactory(target)에서 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨주고 있는 것을 볼 수 있다. 프록시 팩토리는 이 인스턴스 정보를 기반으로 프록시를 만들어 내고, 만약 이 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성하는 것이다.
여기서 호출 대상이 ServiceImpl이고 해당 구현체는 ServiceInterface 인터페이스를 구현하고 있으므로 해당 인터페이스를 기반으로 한 JDK 동적 프록시를 생성한다.
그 후, proxyFactory.addAdvice(new TimeAdvice())를 통해 프록시 팩토리를 통해서 만든 프록시가 사용할 부가기능 로직을 설정한다. 프록시 팩토리에서 MethodInterceptor를 설정해줄 때는 일반적인 DI처럼 세터를 사용하는 대신, 이렇게 addAdvice()라는 메서드를 사용한다. 프록시 팩토리에는 여러 개의 MethodInterceptor를 추가가 가능하고, 이는 프록시 팩토리 하나만으로 여러 개의 부가기능을 제공하는 프록시를 생성할 수 있다는 말이다.
쉽게 생각하면, JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는
MethodInterceptor 의 개념과 유사하다. 이렇게 프록시가 제공하는 부가 기능 로직을 어드바이스(Advice)라 한다.
이번에는 CGLIB를 적용한 테스트 코드를 작성해보자.
@DisplayName("구체 클래스가 있으면 CGLIB 사용")
@Test
void concreteProxy() {
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
/**
* 13:35:45.647 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ConcreteService
* 13:35:45.648 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class com.example.advanced_spring.proxy.common.service.ConcreteService$$SpringCGLIB$$0
* 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ConcreteService -- ConcreteService 호출...
* 13:35:45.649 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
}
ConcreteService$$SpringCGLIB$$0를 보면 CGLIB 프록시가 정상적으로 적용된 것을 확인할 수 있다.
이제 ProxyTargetClass 옵션을 사용해서 인터페이스가 있어도 CGLIB 프록시를 생성하도록 처리해보자. 해당 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.
@DisplayName("ProxyTargetClass 옵션을 이용하면 인터페이스가 있어도 CGLIB를 사용")
@Test
void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true); // 옵션 설정
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)).isFalse();
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
/**
* 13:36:21.783 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
* 13:36:21.785 [Test worker] INFO com.example.advanced_spring.proxy.proxyfactory.ProxyFactoryTest -- proxyClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl$$SpringCGLIB$$0
* 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
* 13:36:21.786 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
}
어드바이스(Advice): 타깃 객체에 적용하는 부가기능을 담은 객체다. 단순하게 프록시 로직이라 생각하면 된다.
포인트컷(PointCut): 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트에 기능을 적용할지 하지 않을지 잘라서 구분하는 것이다.
어드바이저(Advisor): 포인트컷 1개와 어드바이스 1개를 합쳐 놓은 것이다. 왜 합친거지…?
간단히 정리하면 부가 기능 로직을 적용해야 하는데, 포인트컷으로 어디에 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택해야 한다. 이 모든 것을 알고 있는 것이 바로 어드바이저(Advisor)이다. 조언자(Advisor)는 어디(Pointcut)에 조언(Advice)을 해야할지 알고 있는 것이다.
이렇게 구분한 것은 역할과 책임을 명확하게 분리한 것이다. 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다. 어드바이스는 깔끔하게 부가 기능 로직만 담당한다. 둘을 합치면 어드바이저가 된다.

호출 흐름을 간단하게 살펴보자면, 일단 클라이언트가 프록시를 호출하게 되면 프록시는 어드바이저 내부의 포인트컷을 호출한다. 메서드를 호출해도 된다는 확인을 받으면 그때 부가기능을 호출하고 필터링에 실패하면 부가기능을 호출하지 않고 바로 타겟 객체를 호출한다.
어드바이저는 내부에 하나의 포인트컷과 하나의 어드바이스를 가지고 있다고 했다. 프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할지 알 수 있다.
package com.example.advanced_spring.proxy.advisor;
import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
@Slf4j
public class AdvisorTest {
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
/**
* 16:41:50.024 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 16:41:50.026 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
* 16:41:50.026 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*
* 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
* 16:41:50.027 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*/
}
}
지금은 Pointcut.TRUE를 넘겨줌으로써 포인트컷의 필터링을 무조건 통과하도록 처리했다. 따라서 실행 시간 측정 부가기능을 수행하는 TimeAdvice가 정상적으로 동작하는 것을 확인할 수 있다.
이번에는 save() 메서드에는 어드바이스를 호출하고, find() 메서드에는 호출하지 않도록 필터링 하는 포인트컷을 직접 만들어보도록 하자. 다행히도 스프링이 아래와 같이 Pointcut 인터페이스를 제공하고 있어서 깡구현은 하지 않아도 된다.
package org.springframework.aop;
public interface Pointcut {
Pointcut TRUE = TruePointcut.INSTANCE;
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
Pointcut 인터페이스는 내부에 ClassFilter와 MethodMatcher로 이루어져 있는데 클래스가 맞는지, 특정 메서드와 매칭이 되는지 확인할 때 사용한다. 이 2개의 관문을 모두 통과해야 어드바이스를 적용할 수 있게 되는 것이다.
Pointcut 인터페이스를 구현한 구현체를 스프링이 제공하지만 연습을 위해 이번만 직접 포인트컷을 만들어보자.
package com.example.advanced_spring.proxy.advisor;
import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
@Slf4j
public class AdvisorTest {
...
@Test
void advisorTest2() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
/**
* 17:01:55.248 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=save, targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
* 17:01:55.251 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=true
* 17:01:55.252 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
* 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*
* 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 호출 method=find, targetClass=class com.example.advanced_spring.proxy.common.service.ServiceImpl
* 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.advisor.AdvisorTest -- 포인트컷 결과 result=false
* 17:01:55.253 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
*/
}
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
static class MyMethodMatcher implements MethodMatcher {
private String matchName = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(matchName);
log.info("포인트컷 호출 method={}, targetClass={}", method.getName(), targetClass);
log.info("포인트컷 결과 result={}", result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
}
이번에는 스프링이 제공하는 NameMatchMethodPointcut 를 사용해서 구현해보자.
package com.example.advanced_spring.proxy.advisor;
import com.example.advanced_spring.proxy.common.advice.TimeAdvice;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import java.lang.reflect.Method;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
@Slf4j
public class AdvisorTest {
...
@Test
void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
/**
* 17:15:55.486 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 실행...
* 17:15:55.489 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
* 17:15:55.489 [Test worker] INFO com.example.advanced_spring.proxy.common.advice.TimeAdvice -- TimeProxy 종료 resultTime=0
*
* 17:15:55.490 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- find 호출...
*/
}
...
}
보다시피 find() 메서드일 경우에는 어드바이스가 호출되지 않는 것을 확인할 수 있다. 이처럼 스프링은 NameMatchMethodPointcut뿐만 아니라 여러 포인트컷 구현체를 제공하고 있다. 대표적인 구현체는 다음과 같다.
NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다. (*xxx* 허용)
JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
TruePointcut : 항상 참을 반환한다.
AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut을 사용하게 된다.
이번에는 처리 흐름을 아래 그림과 같이 타깃 객체에 여러 어드바이저를 적용하는 코드를 작성해보자.

package com.example.advanced_spring.proxy.advisor;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class MultiAdvisorTest {
@Test
void multiAdvisorTest1() {
// 프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory1.getProxy();
// 프록시2 생성
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory1.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
// 실행
proxy2.save();
/**
* 23:33:12.266 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice1 -- advice1 호출...
* 23:33:12.267 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice2 -- advice2 호출...
* 23:33:12.268 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
*/
}
@Slf4j
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice1 호출...");
return invocation.proceed();
}
}
@Slf4j
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice2 호출...");
return invocation.proceed();
}
}
}
근데 위와 같이 여러 부가기능을 수행하기 위해서는 그 부가기능만큼 프록시를 생성하는 코드를 작성해야 하는 문제점이 있다. 다행히도 스프링은 하나의 프록시에 여러 어드바이저를 적용할 수 있도록 되어 있다. 그냥 적용할 어드바이저를 addAdvisor() 메서드를 이용해서 추가하기만 하면 된다.
package com.example.advanced_spring.proxy.advisor;
import com.example.advanced_spring.proxy.common.service.ServiceImpl;
import com.example.advanced_spring.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
public class MultiAdvisorTest {
...
@Test
void multiAdvisorTest2() {
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor1);
proxyFactory1.addAdvisor(advisor2);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
// 실행
proxy.save();
/**
* 23:40:21.378 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice1 -- advice1 호출...
* 23:40:21.380 [Test worker] INFO com.example.advanced_spring.proxy.advisor.MultiAdvisorTest$Advice2 -- advice2 호출...
* 23:40:21.380 [Test worker] INFO com.example.advanced_spring.proxy.common.service.ServiceImpl -- save 호출...
*/
}
...
}

보다시피 multiAdvisorTest1(), multiAdvisorTest2() 모두 여러 개의 어드바이저를 적용할 수 있지만, multiAdvisorTest2()처럼 addAdvisor()를 사용해서 적용할 어드바이저를 추가하는 것이 코드량도 훨씬 적고 성능도 더 좋다.
이제 프록시 팩토리를 사용해서 애플리케이션에 프록시를 생성해보자.
package com.example.advanced_spring.proxy.config.v3_proxyfactory.advice;
import com.example.advanced_spring.trace.TraceStatus;
import com.example.advanced_spring.trace.logtrace.LogTrace;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class LogTraceAdvice implements MethodInterceptor {
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try {
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getName() + "." + method.getName() + "()";
status = logTrace.begin(message);
// 로직 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
// 포인트컷
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
// 어드바이스
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
현재 V1은 인터페이스만 존재하고 있기 때문에 애플리케이션을 실행해보면 프록시 팩토리가 JDK 동적 프록시가 적용되는 것을 확인할 수 있다.
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
private Advisor getAdvisor(LogTrace logTrace) {
// 포인트컷
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
// 어드바이스
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
V2는 인터페이스가 없고 구현체만 존재하고 있기 때문에 프록시 팩토리가 CGLIB을 적용하는 것을 확인할 수 있다. 이로써 번거롭게 프록시를 일일이 생성해주지 않고 프록시 팩토리가 상황에 맞게 알맞은 방법으로 프록시를 생성해주도록 위임할 수 있게 되었고, 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가기능을 어디에 적용해야 할지 아주 명확하게 이해할 수 있게 되었다.
하지만 아직 남은 문제들이 몇 가지 남았는데, 위의 코드만 보더라도 각 방식을 적용하기 위해서는 설정 파일들을 일일이 만들어줘야 한다는 것이다. 스프링 빈으로 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어줘야 한다. 그리고 컴포넌트 스캔을 사용하는 경우 지금까지 알고 있는 방법으로는 프록시 적용이 불가능하다. 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.