이전 글에서 인터페이스가 있는 경우에는 JDK 동적 프록시, 그렇지 않은 경우에는 CGLIB을 적용했음.
이는 같은 기능을 하지만 인터페이스 여부에 따라 다른 구현을 해야 하므로 번거롭고 중복 코드 문제가 발생
프시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB을 사용하도록 제공
프록시 팩토리의 플로우
Client의 요청이 오면 프록시 팩토리는 인터페이스 여부에 따라 JDK 동적 프록시를 사용할지 CGLIB을 사용할지 결정
그리고 그 결과를 클라이언트에 반환 함
Advice
JDK 동적 프록시는 Invocation을 사용하고 CGLIB은 MethodInterceptor를 사용하는데 이 차이를 프록시 팩토리는 어떻게 극복할까
프록시 팩토리는 Client의 요청을 받으면 인터페이스 유무에 따라 jdk proxy, cglib proxy을 각각 구현한 Handler로 요청이 가게 함
그러면 각 InvocationHandler, MethodInterceptor 모두 결국에는 Advice를 호출하게 만들어, 개발자는 Advice만 만들어 주면 됨
Advice란 프록시에 적용하는 "부가 기능" 로직을 의미
이는 이전에 InvocationHandler, MethodInterceptor의 개념과 유사함
Advice 구현 방법
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeAdvice 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeAdvice 종료 resultTime={}ms", resultTime);
return result;
}
}
구현해야 하는게 CGLIB에서 사용하는 MethodInterceptor와 이름이 같으니 주의
MethodInterceptor -> Intertceptor -> Advice 이런식으로 구조가 되어 있음
기존 InvoactionHandler, MethodInterceptor와 다르게 target과 args가 없는데, 이는 파라미터에 있는 invocation에 이미 들어가 있고 이는 프록시 팩토리로 프록시를 생성해주는 앞 단계에서 이미 target 정보를 파라미터로 전달함
invocation에 모든 정보가 들어있기 때문에 여기서는 단순히 invocation.proceed()만 수행해주면 됨
프록시 팩토리로 프록시 생성 방법
@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();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
}
new ProxyFactory(target)으로 프록시 팩토리를 생성할 때 target을 넣어 줌
프록시 팩토리에 addAdvice로 앞서 만든 advice를 넣어 줌
프록시 팩토리가 만들어지면 getProxy를 통해 프록시를 가져올 수 있음.
프록시의 타겟이 Interface였기 때문에 자동으로 Cglib이 아닌 Jdk 동적 프록시가 만들어 졌기에 isJdkDynamicProxy는 true고 isCglibProxy는 false
만약 Interface라도 CGLIB으로 만들고 싶으면 proxyFactory.setProxyTargetClass(true); 설정을 해주면 됨
포인트컷, 어드바이스, 어드바이저
포인트컷( Pointcut )
어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
주로 클래스와 메서드 이름으로 필터링
이름 그대로 어느 포인트에 적용할지 딱 컷해서 지정
어드바이스( Advice )
이전에 본 것 처럼 프록시가 호출하는 부가 기능
단순하게 프록시 로직이라고 보면 됨
어드바이저( Advisor )
하나의 포인트컷과 하나의 어드바이스
어드바이저 = 포인트컷1 + 어드바이스1
어드바이저(조언자)는 어디(포인트컷)에 어떤 조언(어드바이스)를 할 것이냐
이는 역할과 책임을 명확하게 분리하는 방법
어드바이저 사용 방법
@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(); // 어드바이스가 적용됩니다.
}
어드바이저는 PointCut + Advice이기 때문에 Advisor를 생성할때는 포인트컷과 어드바이스를 넣어 줘야 함
Pointcut.TRUE로 세팅하면 모두에 적용한다는 의미
이전의 코드에서는 proxyFactory.addAdvice(new TimeAdvice()) 이런식으로 사용했었는데, 단순히 Advice만 넣으면 Pointcut.TRUE가 적용된 어드바이저가 사용되는 것
커스텀 포인트컷
@Slf4j
public static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
@Slf4j
public 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) {
throw new UnsupportedOperationException();
}
}
메서드 이름이 save면 true가 나오도록 포인트컷을 사용
실제 포인트컷을 직접 구현해서 사용할일은 거의 없음
사용을 한다면 스프링이 제공하는 포인트컷을 사용하면 됨
위와 같은 커스텀 포인트컷을 만들면 new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice()) 와 같이 사용 가능
스프링이 제공하는 포인트컷
@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 pointcut = new NameMatchMethodPointcut(); 으로 pointcut 생성
pointcut.setMappedNames("save"); 으로 매칭할 method name을 입력
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice()) 으로 pointcut 을 Advisor 생성시 넣어 줌
스프링이 제공하는 포인트 컷
NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭합니다. 내부적으로PatternMatchUtils를 사용합니다. 예)*xxx*와 같은 패턴을 허용합니다.
JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭합니다.
TruePointcut: 항상 참을 반환합니다.
AnnotationMatchingPointcut: 애노테이션을 기반으로 매칭합니다.
AspectJExpressionPointcut: AspectJ 표현식을 기반으로 매칭합니다.
사실상 실무에서는 AspectJ 표현식 기반을 거의 사용하며 이는 추후 AOP 부분에서 자세히 설명
하나의 타겟에 여러 어드바이저 적용
기존에 사용하던 코드를 활용해서 Proxy를 2개 만들어서 target은 proxy1로 감싸고, proxy1은 proxy2로 감싸면 하나의 target에 두 개의 어드바이저를 적용 가능
만약 10개의 어드바이저를 적용하려면 10개의 프록시를 생성해야하는 문제 발생
하나의 타겟에 여러 어드바이저 적용 개선 방법
@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();
}
프록시 팩토리에 addAdvisor로 여러 어드바이저 추가 가능
이 때 넣어준 순서대로 동작
이 부분에서 중요한 점은 추후에 나올 AOP에서 AOP 적용 수 만큼 프록시가 생성된다는 착각을 방지하기 위해
이 앞전의 예제처럼 여러 프록시를 만들어서 어드바이저를 적용하는 방법도 존재
하지만, 스프링 AOP는 target 마다 하나의 프록시만 생성하는 방법을 사용
프록시 팩토리를 LogTrace에 적용
@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;
}
}
}
Advice를 생성
@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) {
// pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
// advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
// advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
Advice와 Pointcut를 적용한 Advisor로 프록시 팩토리를 만들어 프록시 적용