프록시 팩토리
문제점 : 인터페이스가 있는 경우에는 JDK 동적 프록시가 제공하는 (InvocationHandler), 인터페이스가 없는 경우에는 CGLIB가 제공하는 (MethodInterceptor)를 각각 만들어서 관리해야할까?
해결 방안 : 동적 프록시를 통합해서 만들어주는 ( ProxyFactory )
- 스프링은 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다.
- 결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
- 프록시 팩토리는 Advice 를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.
- 특정 로직을 체크해서 프록시를 적용할지 말지 결정하는 Pointcut 개념 도입
프록시 팩토리 - 예제
MethodInterceptor - 스프링이 제공하는 코드
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
- Advice 를 만드는 방법 - MethodInterceptor 구현
TimeAdvice
@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={}ms", resultTime);
return result;
}
}
- Object result = invocation.proceed()
invocation.proceed() 를 호출하면 target 클래스를 호출하고 그 결과를 받는다.
ProxyFactory로 프록시를 생성하는 단계에서 이미 target 정보를 파라미터로 전달받기 때문에 target 정보 X.
ProxyFactoryTest
//JDK 동적 프록시 적용 Test
//CGLIB 적용 Test
//인터페이스 있어도 CGLIB 강제 적용
@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();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
- proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제 CGLIB 적용시킨다.
실행 결과
//JDK 동적 프록시 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy13
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
//CGLIB 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ConcreteService
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$103821ba
TimeAdvice - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
//인터페이스 있어도 강제로 CGLIB 적용
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ServiceImpl$$EnhancerBySpringCGLIB$$2bbf51ab
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
★ 프록시 팩토리의 기술 선택 방법
- 대상에 인터페이스가 있으면: JDK 동적 프록시, 인터페이스 기반 프록시
- 대상에 인터페이스가 없으면: CGLIB, 구체 클래스 기반 프록시
- proxyTargetClass=true : CGLIB, 구체 클래스 기반 프록시, 인터페이스 여부와 상관없음
★ 정리
- ProxyFactory 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가 Advice 를 호출하도록 개발해두고,
- CGLIB인 경우 MethodInterceptor 가 Advice 를 호출하도록 기능을 개발해두었기 때문이다.
포인트컷, 어드바이스, 어드바이저 - 소개
- 포인트컷( Pointcut ) : 부가 기능을 어디에 적용할지 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다.
이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다. - 어드바이스( Advice ) : 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다.
- 어드바이저( Advisor ) : 단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 포인트컷1 + 어드바이스1이다.
※ 역할과 책임
- 포인트컷은 대상 여부를 확인하는 필터 역할만 담당한다.
- 어드바이스는 깔끔하게 부가 기능 로직만 담당한다.
- 둘을 합치면 어드바이저가 된다. 스프링의 어드바이저는 하나의 포인트컷 + 하나의 어드바이스로 구성된다.
Advisor - 예제
Pointcut 관련 인터페이스 - 스프링 제공
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
public interface ClassFilter {
boolean matches(Class<?> clazz);
}
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass);
//..
}
AdvisorTest
@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();
}
- NameMatchMethodPointcut 을 생성하고 setMappedNames(...) 으로 메서드 이름을 지정하면 포인트컷이 완성된다.
스프링이 제공하는 포인트컷
- NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다.
ex) *xxx* 허용 - JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut : 항상 참을 반환한다.
- AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
여러 어드바이저 함께 적용
MultiAdvisorTest
@Test
@DisplayName("여러 프록시") void multiAdvisorTest1() {
//client -> proxy2(advisor2) -> proxy1(advisor1) -> target
//프록시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 생성, target -> proxy1 입력
ProxyFactory proxyFactory2 = new ProxyFactory(proxy1);
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.getProxy();
//실행
proxy2.save();
}
문제 : 프록시 N개 생성해야 한다. 즉, Advisor (N개) + Proxy (N개) 만들어야한다.
해결 방안 : 하나의 프록시, 여러 어드바이저
MultiAdvisorTest
@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 마다 하나의 프록시만 생성한다.
ProxyFactory - 적용
LogTraceAdvice - 인터페이스가 있는 경우
@Slf4j
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;
}
}
}
ProxyFactoryConfig
@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;
}
// OrderServiceV1
// OrderRepositoryV1
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
애플리케이션 로딩 로그 - JDK 동적 프록시 적용
ProxyFactory proxy=class com.sun.proxy.$Proxy50,
target=class ...v1.OrderRepositoryV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy52,
target=class ...v1.OrderServiceV1Impl
ProxyFactory proxy=class com.sun.proxy.$Proxy53,
target=class ...v1.OrderControllerV1Impl
@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;
}
// OrderServiceV1
// OrderRepositoryV1
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
애플리케이션 로딩 로그
ProxyFactory proxy=class hello.proxy.app.v2.OrderRepositoryV2$
$EnhancerBySpringCGLIB$$594e4e8, target=class
hello.proxy.app.v2.OrderRepositoryV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderServiceV2$
$EnhancerBySpringCGLIB$$59e5130b, target=class
hello.proxy.app.v2.OrderServiceV2
ProxyFactory proxy=class hello.proxy.app.v2.OrderControllerV2$
$EnhancerBySpringCGLIB$$79c0b9e, target=class
hello.proxy.app.v2.OrderControllerV2
※ 정리 : 어드바이저, 어드바이스, 포인트컷 덕분에 어떤 부가 기능을 어디에 적용할 지 명확해졌다.
문제 1 : 너무 많은 설정
- ProxyFactoryConfig 같은 설정 파일이 너무 많다.
문제 2 : 컴포넌트 스캔
- 컴포넌트 스캔을 사용하는 경우 프록시 적용이 불가능하다.
- 왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
해결 방안 : 빈 후처리기
'Spring > Advanced' 카테고리의 다른 글
@Aspect AOP (0) | 2023.08.21 |
---|---|
빈 후처리기 (0) | 2023.08.17 |
동적 프록시 기술 (0) | 2023.08.16 |
프록시 패턴과 데코레이터 패턴 (0) | 2023.08.14 |
템플릿 메서드 패턴과 콜백 패턴 (0) | 2023.08.13 |