[Spring AOP] 2.ProxyFactory로 대상에 따른 기술에 구애받지 않고 Proxy 생성,적용하기 (Advisor, Advice, PointCut)
해당 내용에 대한 이해를 위해 사전 지식을 원한다면 링크를 참고해주세요.
참고글 : https://nomoreft.tistory.com/107
ProxyFactory, Advice, Pointcut
개발자는 ProxyFactory로 Interface인지, Class인지 구분하지 않고, Advice
를 등록하면 된다.
ProxyFactory는 Interface가 있으면 JDK Proxy를 생성하고, 없으면 CGLIB를 생성해서 Client에게 제공한다.
ProxyFactory로 생성된 Proxy는 Advice
를 호출한다.
그래서 사용자는 Proxy의 종류에 구애받지 않고 Advice
로 공통기능을 작성하면 된다.
특정 메서드 패턴만 작동한다던지 동작에 조건을 걸때는 PointCut
을 사용하면 된다.
PointCut
: 어디에 부가기능을 적용할지, 어디에 부가기능을 적용하지 않을지 판단하는 필터링 로직이다.
주로 클래스와 메서드 이름으로 필터링 한다. 이름 그대로 어떤 포인트(Point)에 기능을 적용할지 안할지 잘라서 구분 (cut)Advice
: 프록시가 호출하는 부가기능. 공통 로직Advisor
: 하나의 포인트컷 + 어드바이스를 가지고 있는 객체. 어드바이스를 어디에 적용할지 결정하는 역할
공통기능인 Advice를 제작하고, 어디에 적용할지 결정하는 PointCut을 만들어서 Advisor에 넣어서 ProxyFactory에 등록한다.
구분한 이유 ? -> 역할과 책임 분배
- 포인트컷 -> 부가 기능 적용 여부 결정하는 필터 기능만 담당
- 어드바이스 -> 부가 기능만 담당
- 둘을 한 세트로 합쳐 어드바이저로 뭉쳐 사용.
> Advice : 프록시 기능
> Pointcut : 특정 조건을 달성할 때만 프록시 기능이 적용되는 것
> JoinPoint : Advice가 적용되는 지점
> Target : Advice가 적용되는 대상
> Weaving : Advice를 Target에 적용하는 것
> AOP : Aspect Oriented Programming
ProxyFactory 사용법
Advice를 정의한다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {
/**
*
* @param invocation the method invocation joinpoint
* @return
* @throws Throwable
*/
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long start = System.currentTimeMillis();
/**
더이상 target을 넣지 않고, invocation에 이미 담겨져있다.
target을 찾아서 원본을 실행해준다,
*/
// Object result = methodProxy.invoke(target, args);
Object result = invocation.proceed();
long end = System.currentTimeMillis();
log.info("TimeProxy 종료");
log.info("TimeProxy 실행 시간: {}ms", end - start);
return result;
}
}
import org.aopalliance.intercept.MethodInterceptor
: Advice를 구현하는 인터페이스- ProxyFactory를 생성할 때 이미 target이 있기 때문에, Advice에서 target을 찾아서 실행할 필요가 없다.
ProxyFactory를 생성한다.
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
/**
* 실제 target으로 ProxyFactory 생성,
* Advice를 추가하여 Proxy 기능을 담는다.
*/
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
//proxy 작동
proxy.save();
/**
* `AopUtils`
* ProxyFactory로 Proxy를 생성했을때 사용 가능하다.
*/
//타겟이 Proxy객체인지 확인
Assertions.assertThat(AopUtils.isAopProxy(proxy)).isTrue();
//타겟이 Proxy객체이면서 인터페이스를 구현한 Proxy인지 확인
Assertions.assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
//타겟이 Proxy객체이면서 클래스를 상속받은 Proxy인지 확인
Assertions.assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
ProxyFactory proxyFactory = new ProxyFactory(target);
: target을 넣어서 최초 생성한다.proxyFactory.addAdvice(new TimeAdvice());
: Advice를 추가한다.ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
: Proxy를 생성한다.AopUtils
: ProxyFactory로 생성된 Proxy를 확인할 수 있다.- Interface가 없는 경우에도 그냥 비슷하게 구현해주면 된다.
결론
ProxyFactory로 추상화 했기 때문에, Proxy 기술에 의존하지 않고 사용할 수 있다.
부가 기능 로직도 Advice
로 분리해서 사용할 수 있다.
InvocationHandler와 MethodInterceptor가 Advice를 호출하도록 되어있기 때문이다.
위의 예제는 Advice 와 PointCut이 함께 구현되어있기 때문에, 둘을 구분하여야 단일책임원칙
을
지켜 따로 생성해야한다.
단일 책임 원칙을 지킨 ProxyFactory
작동 순서
(1)client -> Proxy 객체 호출
(2)-> Proxy 객체(ProxyFactory로 생성한) 는 Advisor 호출
(3)-> Advisor는 PointCut 필터를 호출
(4)-> PointCut 필터에 통과되어 적용 가능하면 Advice를 호출
(5)-> Advice는 InvocationHandler 또는 MethodInterceptor를 호출
(6)-> InvocationHandler 또는 MethodInterceptor는 타겟을 호출
(7)-> 타겟은 실제 로직(Method)을 수행
구현
target
public interface ServiceInterface {
void save();
void find();
}
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
System.out.println("save");
}
@Override
public void find() {
log.info("find 호출");
}
}
- save, find 메서드를 가진 구현체이다.
ProxyFactory + Advisor
@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
/**
* 메소드 네임이 save인 경우에만 허용
*/
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
nameMatchMethodPointcut.setMappedName("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(nameMatchMethodPointcut , new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
new ProxyFactory(target)
: 타겟을 넣어 프록시 팩토리를 생성한다.new TimeAdvice()
:import org.aopalliance.intercept.MethodInterceptor
의 invoke 메서드를 구현한 구현체이다. (Advice)new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice())
: 기본 PointCut을 가진 Advisor를 생성한다.proxyFactory.addAdvisor(advisor)
: Advisor를 추가한다.ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
: Proxy를 생성한다.
PointCut 종류
NameMatchMethodPointcut
: 메서드 이름으로 허가JdkRegexpMethodPointcut
: 정규식으로 허가TruePointcut
: 모든 메서드 허가AnnotationMatchingPointcut
: 어노테이션으로 허가AspectJExpressionPointcut
: AspectJ 표현식으로 허가
실무에서는 AspectJExpressionPointcut
: AspectJ 표현식으로 허가 를 많이 사용한다.
MultiAdvisor
Target에 여러 부가기능이 달리게 한다.
advisor1,2 제작
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("Advice1 실행");
return invocation.proceed();
}
}
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("Advice2 실행");
return invocation.proceed();
}
}
적용
@Test
@DisplayName("proxy -> multiAdvisor")
void multiAdvisorTest2() {
//advisor1,2 생성
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
//등록 순서대로 먼저 실행된다 proxy2 -> proxy1
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
}
💡참고사항
Spring AOP에서도 이와 같이 객체당 Proxy 객체는 하나만 만들고,
Advisor를 여러개 적용하여 넣는 방식으로 사용된다. (Proxy 객체가 여러개 생긴다고 생각하면 안됨)
적용 예시
private Advisor getAdvisor(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
---
위에 방식들로 수동으로 구현해서 하는 경우는 없을 것이다. 그러나 추후에 배울 AOP 내용에 대한 이해를 위해
꼭 필요한 개념이라 판단된다.