내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
이번 글 에서는 지난번 ProxyFactory에 이어서 포인트컷, 어드바이스, 어드바이저 에 대하여 학습해보자!
4. 포인트컷, 어드바이스, 어드바이저 - 소개
스프링 AOP를 공부했다면 다음과 같은 단어를 들어보았을 것이다.
포인트컷( Pointcut )
어디에 부가 기능을 적용할지, 말지를 판단하는 필터링 로직이다. 주로 클래스와 메서드 이름으로 필터링 한다.
이름 그대로 어떤 포인트(Point)에 기능을 적용할지 하지 않을지 잘라서(cut) 구분하는 것이다.
어드바이스( Advice )
이전에 본 것 처럼 프록시가 호출하는 부가 기능이다. 단순하게 프록시 로직이라 생각하면 된다.
어드바이저( Advisor )
단순하게 하나의 포인트컷과 하나의 어드바이스를 가지고 있는 것이다. 쉽게 이야기해서 포인트컷1 + 어드바이스1이다.
정리하면 부가 기능 로직을 적용해야 하는데, 포인트컷으로 어디에? 적용할지 선택하고, 어드바이스로 어떤 로직을 적용할지 선택하는 것이다.
그리고 어디에? 어떤 로직?을 모두 알고 있는 것이 어드바이저이다.
다음 그림을 하나 살펴보자.
그림은 이해를 돕기 위한 것이고, 실제 구현은 약간 다를 수 있다.
이후 AOP 파트에서 다시한번 설명하도록 해보자!
5. 예제 코드1 - 어드바이저
프록시 팩토리를 통해 프록시를 생성할 때 어드바이저를 제공하면 어디에 어떤 기능을 제공할 지 알 수 있다!
우선 다음 테스트코드를 살펴보자.
@Test
public 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();
}
일반적은 Advisor의 구현체로는 "new DefaultPointcutAdvisor" 를 사용한다.
생성자를 통해 하나의 포인터 컷과 어드바이스 를 전달하면 된다.
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
포인트 컷으로 TRUE를 전달했는데, 모든 대상에 적용하겠다는 의미이다.
이후 프록시 팩토리에 적용할 어드바이져를 지정한다.
proxyFactory.addAdvisor(advisor);
어드바이져 안에는 포인트컷 과 어드바이스 둘다 들어있기 때문에 어디에 어떤 부가 기능을 적용시킬지 모두 알고있게 된다.
그런데 생각해보면 이전에 분명히 proxyFactory.addAdvice(new TimeAdvice())처럼 어드바이저가 아니라 어드바이스를 바로 적용했다.
이것은 단순히 편의 메서드이고 결과적으로 해당 메서드 내부에서 지금 코드와 똑같은 다음 어드바이저가 생성된다.
addAdvice 내부를 보면, DefaultPointcutAdvisor()를 호출하게 된다.
DefaultPointcutAdvisor 내부에서는 다음과 같이 Pointcut.TRUE로 생성하여 반환하게 된다.
테스트 코드 실행시 다음과 같은 결과를 확인할 수 있다.
find, save 모두에 추가로직이 적용된것을 확인할 수 있다.
6. 예제 코드2 - 직접 만든 포인트컷
이번에는 save() 메서드에는 어드바이스 로직을 적용하지만, find() 메서드에는 어드바이스 로직을 적용하지 않도록 해보자.
직접 포인트컷을 만들어서 사용해볼 것 이다.
포인트컷을 직접 만들기 위해서는 Pointcut Interface를 구현해야 한다.
▶ 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 로 반환해야 어드바이스를 적용할 수 있다.
▶ MyPointcut
static class MyPointcut implements Pointcut {
@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}
@Override
public MethodMatcher getMethodMatcher() {
return new MyMethodMatcher();
}
}
일단 모든 class에 대하여 허용할것이기 때문에 TRUE를 반환해준다.
다만 메서드는 Matcher를 통해 필터링 해야 한다.
▶ MyMethodMatcher
@Slf4j
static class MyMethodMatcher implements MethodMatcher {
private static final String MATCH_NAME = "save";
@Override
public boolean matches(Method method, Class<?> targetClass) {
boolean result = method.getName().equals(MATCH_NAME);
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;
}
}
1) matches()
이 메서드에 method , targetClass 정보가 넘어온다. 이 정보로 어드바이스를 적용할지 적용하지 않을지 판단할 수 있다.
우리의 코드에서는 메서드 이름이 "save" 인 경우에 true 를 반환하도록 판단 로직을 적용했다.
boolean result = method.getName().equals(MATCH_NAME);
2) isRuntime()
isRuntime() 이 값이 참이면 matches(... args) 메서드가 대신 호출된다.
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
return false;
}
동적으로 넘어오는 매개변수를 판단 로직으로 사용할 수 있다.
isRuntime() 이 false 인 경우 클래스의 정적 정보만 사용하기 때문에 스프링이 내부에서 캐싱을 통해 성능 향상이 가능하지만, isRuntime() 이 true 인 경우 매개변수가 동적으로 변경된다고 가정하기 때문에 캐싱을 하지 않는다.
MyPointcut을 ProxyFactory에 전달하여 프록시를 생성해보자!
@DisplayName("직접 만든 포인트컷")
@Test
public 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();
}
실행 결과는 다음과 같다.
실행 결과를 보면 기대한 것과 같이 save() 를 호출할 때는 어드바이스가 적용되지만, find() 를 호출할 때는 어드바이스가 적용되지 않는다.
마지막으로 save()가 호출되는 과정을 그림으로 정리해보자!
- 클라이언트가 프록시의 save() 를 호출한다.
- 포인트컷에 Service 클래스의 save() 메서드에 어드바이스를 적용해도 될지 물어본다.
- 포인트컷이 true 를 반환한다. 따라서 어드바이스를 호출해서 부가 기능을 적용한다.
- 이후 실제 인스턴스의 save() 를 호출한다.
find()의 경우 포인트컷에서 걸려서 Advice의 부가 기능이 적용되지 않고, 바로 find()메서드만 호출하게 된다.
7. 예제 코드3 - 스프링이 제공하는 포인트컷
이번에는 스프링이 제공하는 NameMatchMethodPointcut 를 사용해서 구현해보자.
@DisplayName("스프링이 제공하는 포인는컷")
@Test
public void advisorTest3() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); // 포인트컷 생성
pointcut.setMappedName("save");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
실행 결과는 다음과 같다.
save에는 어드바이스의 시간측정 로직이 적용되었고, find에는 적용되지 않았다!
▶ Spring이 제공하는 Pointcut
- NameMatchMethodPointcut : 메서드 이름을 기반으로 매칭한다. 내부에서는 PatternMatchUtils 를 사용한다.
- JdkRegexpMethodPointcut : JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
- TruePointcut : 항상 참을 반환한다.
- AnnotationMatchingPointcut : 애노테이션으로 매칭한다.
- AspectJExpressionPointcut : aspectJ 표현식으로 매칭한다.
가장 중요한 포인트컷은 aspectJ 이다.
실무에서는 사용하기도 편리하고 기능도 가장 많은 aspectJ 표현식을 기반으로 사용하는 AspectJExpressionPointcut 을 사용하게 된다.
이에 대한 자세한 설명은 뒤 AOP에서 다룬다고 하셨다.
8. 예제 코드4 - 여러 어드바이저 함께 적용
어드바이저는 하나의 포인트컷과 하나의 어드바이스를 가지고 있다.
만약 여러 어드바이저를 하나의 target 에 적용하려면 어떻게 해야할까?
가장 먼저 떠오르는 간단한 방법은 Proxy자체를 여러개 만드는 방식이다.
다음 그림처럼 말이다.
코드로 살펴보자.
public class MultiAdvisorTest {
@Test
@DisplayName("여러 프록시")
public 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());
proxyFactory2.addAdvisor(advisor2);
ServiceInterface proxy2 = (ServiceInterface) proxyFactory2.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();
}
}
}
포인트컷은 advisor1 , advisor2 모두 항상 true 를 반환하도록 설정했다. 따라서 둘다 어드바이스가 적용된다.
위와 같은 방법이 잘못된 것은 아니지만, 프록시를 2번 생성해야 한다는 문제가 있다.
만약 적용해야 하는 어드바이저가 10개라면 10개의 프록시를 생성해야한다.
Spring에서는 이 문제를 해결하기 위해 1개의 Proxy에 N개의 Advisor를 적용 가능하도록 만들었다.
이를 활용하는 코드는 다음과 같다.
@Test
@DisplayName("하나의 프록시에 여러 어드바이져 적용")
public void multiAdvisorTest2() {
// client -> proxy -> advisor2 -> advisor1 -> target
// 프록시 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 어드바이저 생성
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
proxyFactory.addAdvisor(advisor2);
proxyFactory.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
// 실행
proxy.save();
}
프록시 팩토리에 원하는 만큼 addAdvisor() 를 통해서 어드바이저를 등록하면 된다.
우리의 코드에서는 advisor1, 2를 등록하게 되었다.
등록하는 순서대로 호출되기 때문에, advisor2 호출 후 1이 호출된다.
실행 결과는 다음과 같다.
스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다.
스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다.
이부분을 꼭 기억해두자.
9. 프록시 팩토리 - 적용1
이번에는 ProxyFactory를 우리의 로그 추적 어플리케이션에 적용시켜보자!
인터페이스가 있는 v1 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.
우선 advice 부터 만들어보자!
▶ LogTraceAdvice
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;
}
}
}
이를 활용하는 Bean 설정파일을 만들어보자
▶ ProxyFactoryConfigV1
@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 proxyFactory = new ProxyFactory(orderRepository);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) proxyFactory.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);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
포인트컷은 NameMatchMethodPointcut을 사용한다. 여기에는 심플 매칭 기능이 있어서 * 을 매칭할 수 있다.
request* , order* , save* : request, order, save 로 시작하는 메서드에 포인트컷은 true 를 반환한다.
이렇게 설정한 이유는 noLog() 메서드에는 어드바이스를 적용하지 않기 위해서 이다.
프록시 팩토리에 각각의 target 과 advisor 를 등록해서 프록시를 생성한다.
이후 다음과 같이 main 클래스 파일에 설정해준 후 실행하면 된다.
@Import(ProxyFactoryConfigV1.class)
초기화 단계에서 다음과 같이 로그가 출력되는것을 확인할 수 있다.
실행 결과또한 우리가 원하던 것처럼 출력된다.
10. 프록시 팩토리 - 적용2
이번에는 인터페이스가 없고, 구체 클래스만 있는 v2 애플리케이션에 LogTrace 기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.
Config 파일만 수정해주면 된다.
기존의 V1이 전부 V2로 변경되었다. V2는 구체 클래스 기반으로 구현되어 있었다.
@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 proxyFactory = new ProxyFactory(orderRepository);
proxyFactory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV2 proxy = (OrderRepositoryV2) proxyFactory.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);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
똑같이 설정파일만 변경해준후 실행하면된다.
실행시 초기화 단계에서 로그출력을 확인할 수 있다.
CGLIB 기술을 통해 만들어진것을 확인 가능하다.
LogFactory를 통해 아주 간단하게 변경할수 있게 되었다.
▶ 남은 문제
1) 너무나 많은 설정 과정
ProxyFactoryConfigV1 , ProxyFactoryConfigV2 와 같은 설정 파일이 지나치게 많다는 점이다.
예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다!
무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.
2) 컴포넌트 스캔
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다.
왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 알아서 스프링 빈으로 등록을 다 해버리기 때문이다.
우리는 실제 스프링 빈이 아닌, 부가 기능을 추가한 Proxy를 빈으로 등록시켜야 한다.
두 가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기이다.
'BackEnd > Spring' 카테고리의 다른 글
[Spring] 빈 후처리기 - 2 (0) | 2022.08.16 |
---|---|
[Spring] 빈 후처리기 - 1 (0) | 2022.08.15 |
[Spring] 스프링이 지원하는 프록시 - 1 (0) | 2022.08.14 |
[Spring] 동적 프록시 기술 - JDK, CGLIB (0) | 2022.08.13 |
[Spring] 인터페이스, 구체 클래스 기반 프록시 (0) | 2022.08.10 |
댓글