동적 프록시를 사용할 때 문제점을 다시 확인해보자.
문제점
InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
를 각각 중복으로 만들어서 관리해야 할까?스프링 지원 프록시
ProxyFactory
)라는 기능을 제공한다.Advice
InvocationHandler
나 MethodInterceptor
를 신경쓰지 않고, Advice
만 만들면 된다. 결과적으로 InvocationHandler
나 MethodInterceptor
는 Advice
를 호출하게 된다.Advice
를 호출하는 전용 InvocationHandler
, MethodInterceptor
를 내부에서 사용한다.Advice 만들기
Advice
는 프록시에 적용하는 부가 기능 로직이다.InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
의 개념과 유사한다. 둘을 개념적으로 추상화 한 것이다.MethodInterceptor
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
MethodInvocation invocation
args
, 메서드 정보 등이 포함되어 있다. 기존에 파라미터로 제공되는 부분들이 이 안으로 모두 들어갔다고 생각하면 된다.MethodInterceptor
는 Interceptor
를 상속하고 Interceptor
는 Advice
인터페이스를 상속한다.TimeAdvice
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
인터페이스를 구현Object result = invocation.proceed()
invocation.proceed()
를 호출하면 target
클래스를 호출하고 그 결과를 받는다.target
클래스의 정보는 MethodInvocation invocation
안에 모두 포함되어 있다.ProxyFactoryTest
package hello.proxy.proxyfactory;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
@Slf4j
public class ProxyFactoryTest {
@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();
}
}
new ProxyFactory(target)
: 프록시 팩토리를 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다. 만약 인스턴스에 인터페이스가 있다면 JDK 동적 프록시 사용, 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다.proxyFactory.addAdvice(new TimeAdvice())
: 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정프록시 팩토리를 통한 프록시 적용 확인
AopUtils.isAopProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되면 JDK 동적 프록시나, CGLIB 모두 참이다.AopUtils.isJdkDynamicProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되고, JDK 동적 프록시인 경우 참AopUtils.isCglibProxy(proxy)
: 프록시 팩토리를 통해서 프록시가 생성되고, CGLIB 동적 프록시인 경우 경우 참ProxyFactoryTest - concreteProxy 추가
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
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();
// AopUtils 프록시 팩토리를 통해서 만든 프록시만 사용 가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
CGLIB 프록시가 적용된 것도 확인할 수 있다.
ProxyFactoryTest - proxyTargetClass 추가
@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
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();
// AopUtils 프록시 팩토리를 통해서 만든 프록시만 사용 가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
proxyTargetClass
라는 옵션을 제공하는데, 이 옵션에 true
값을 넣으면 인터페이스가있어도 강제로 CGLIB를 사용한다.프록시 팩토리의 기술 선택 방법
proxyTargetClass=true
: CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음스프링 부트는 AOP를 적용할 때 기본적으로
proxyTargetClass=true
로 설정해서 사용한다.
따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
조언(Advice
)을 어디(Pointcut
)에 할 것인가?
조언자(Advisor
)는 어디(Pointcut
)에 조언(Advice
)을 해야할지 알고 있다.
하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
AdvisorTest
package hello.proxy.advisor;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
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 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();
}
}
new DefaultPointcutAdvisor
: Advisor 인터페이스의 가장 일반적인 구현체이다. 하나의 포인트 컷과 하나의 어드바이스를 넣어주면 된다.Pointcut.TRUE
: 항상 true를 반환하는 포인트 컷proxyFactory.addAdvisor(advisor)
: 프록시 팩토리에 적용할 어드바이저를 지정save()
메서드에는 어드바이스 로직을 적용하지만, find()
메서드에는 어드바이스 로직을 적용하지 않도록 해보자.
Pointcut 관련 인터페이스 - 스프링 제공
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
//..
}
ClassFilter
와 MethodMatcher
둘로 이루어진다. 이름 그대로 하나는 클래스가 맞는지, 하나는 메서드가 맞는지 확인할 때 사용한다. 둘다 true
로 반환해야 어드바이스를 적용할 수 있다.
AdvisorTest - advisorTest2() 추가
@Test
@DisplayName("직접 만들어보는 포인트 컷")
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();
// save만 적용
proxy.save();
proxy.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.getName());
log.info("포인트컷 결과 result={}",result);
return result;
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
}
MyPointcut
true
를 반환하도록 했고, 메서드 비교 기능은 MyMethodMatcher
를 사용한다.MyMethodMatcher
matches()
: 이 메서드에 method
, targetClass
정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다.save() 호출
find()
는 false를 반환하기 때문에 Advice 부가 기능이 적용되지 않는다.AdvisorTest - advisorTest3() 추가
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceImpl 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();
}
스프링은 무수히 많은 포인트컷을 제공한다.
가장 많이 사용
AspectJExpressionPointcut
: aspectJ 표현식으로 매칭한다.
어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
만약 여러 어드바이저를 하나의 target
에 적용하려면 어떻게 해야할까?
여러 프록시 만들기
MultiAdvisorTest
package hello.proxy.advisor;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
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
@DisplayName("여러 프록시 생성")
void multiAdvisorTest1(){
// client -> proxy2 -> proxy1 -> target
// proxy1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy1 = (ServiceInterface) proxyFactory.getProxy();
// proxy2 생성 ( target -> proxy1 입력)
ProxyFactory proxyFactory1 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory1.getProxy();
// 실행
proxy2.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();
}
}
}
하나의 프록시, 여러 어드바이저
multiAdvisorTest2() 추가
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
//proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
// 프록시 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy.save();
}
하나의
target
에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는target
마다 하나의 프록시만 생성한다. 이부분을 꼭 기억해두자.
는 v1 애플리케이션에 LogTrace
기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.
어드바이스 생성
LogTraceAdvice
package hello.proxy.config.v3_proxyfactory;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;
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().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
ProxyFactoryConfigV1
package hello.proxy.config.v3_proxyfactory;
import hello.proxy.app.v1.*;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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){
OrderRepositoryV1Impl 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);
}
}
NameMatchMethodPointcut
), 어드바이스(LogTraceAdvice
)를 가지고 있다.target
과 advisor
를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.애플리케이션에 적용하고
localhost:8080/v1/request?itemId=hello 실행시
V1 애플리케이션은 인터페이스가 있기 때문에 프록시 팩토리가 JDK 동적 프록시를 적용된 것을 확인 할 수 있다.
구체 클래스만 있는 v2 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.
package hello.proxy.config.v3_proxyfactory;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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을 적용한 것을 확인 할 수 있다.
참고
김영한: 스프링 핵심 원리 - 고급편(인프런)
Github - https://github.com/b2b2004/Spring_ex