스프링이 제공하는 빈 후처리기
스프링이 제공하는 빈 후처리기를 사용하기 위해서는 아래의 의존성을 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
위 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. aspectJ 는 뒤에서 설명한다. 스프링 부트가 활성화하는 빈은 AopAutoConfiguration를 참고하자.
(AopAutoConfiguration 더보기)
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Advice.class)
static class AspectJAutoProxyingConfiguration {
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
static class JdkDynamicAutoProxyConfiguration {
}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class CglibAutoProxyConfiguration {
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
matchIfMissing = true)
static class ClassProxyingConfiguration {
@Bean
static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
return (beanFactory) -> {
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
};
}
}
}
자동 프록시 생성기 - AutoProxyCreator
• 앞서 이야기한 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
• 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
• 이 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
• Advisor 안에는 Pointcut 과 Advice가 이미 모두 포함되어 있다. 따라서 Advisor 만 알고 있으면 그 안에 있는 Pointcut으로 어떤 스프링빈에 프록시를 적용해야 할지 알 수 있다. 그리고 Advice로 부가기능을 적용하면 된다.
참고
AnnotationAwareAspectJAutoProxyCreator 는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다.
Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.
![](https://blog.kakaocdn.net/dn/bXJwjj/btrqn9ZOolL/zKONsRHYKARABRHN9aHK11/img.png)
1. 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
2. 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 모든 Advisor 빈 조회 : 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
4. 프록시 적용 대상 체크 : 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 특정 빈 객체의 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 해당 빈 객체는 프록시 적용 대상이 된다.
5. 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
6. 빈 등록 : 반환된 객체는 스프링 빈으로 등록된다.
생성된 프록시
( 프록시는 내부에 어드바이저와 실제 호출해야할 대상 객체( target )을 알고 있다. )
1. 스프링이 시작되면서 빈 객체들이 생성된다.
2. 스프링 컨테이너에 빈 객체들을 넣기 전에 빈 후처리기로 빈 객체들을 보낸다.
3. 모든 빈 객체들이 프록시가 만들어져야 하는지 검사를 받는다.
4. 이때 검사는 빈 후처리기에 있는 어드바이저의 포인트컷에 의해 검사되는 것이다.
5. 만약 포인트컷에 의해 프록시 객체가 만들어져야 한다고 판단되면 원래의 빈객체는 target이 되고,
6. 어드바이저의 어드바이스 로직을 추가해 프록시 객체가 생성된다.
7. (이때 만들어진 프록시 객체에는 어드바이스와 target을 가지고 있다.)
8. 기존의 빈 객체가 아닌 방금 만들어진 프록시 객체가 이름만 그대로 가지고 스프링 컨테이너에 저장된다.
이제 어플리케이션에 직접 적용해보자. 사실 aop 라이브러리를 그래들에 추가하여 스프링이 제공하는 빈 후처리기인 AnnotationAwareAspectJAutoProxyCreator가 이미 빈으로 등록되어 우린 어드바이저만 만들어주면 된다.
@Slf4j
@Import({AppV1Config.class, AppV2Config.class})
@Configuration
public class AutoProxyConfig {
@Bean
public Advisor advisor1(LogTrace logTrace) {
//pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
위처럼 advisor를 빈으로 등록하고
http://localhost:8080/v1/request?itemId=ppo
http://localhost:8080/v2/request?itemId=ppo
http://localhost:8080/v3/request?itemId=ppo
위의 요청들을 날려보면 잘 동작한다.
또한
http://localhost:8080/v1/no-log 으로 요청을 날리면 로그가 출력되지 않는다. (왜냐하면 위의 어드바이저 코드에서 pointcut.setMappedNames에 "request*", "order*", "save*"만 적용하도록 했으므로)
중요: 포인트컷은 2가지에 사용된다.
1. 프록시 적용 여부 판단 - 생성 단계
자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
클래스 + 메서드 조건을 모두 비교한다. 이때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
예) orderControllerV1 은 request() , noLog() 가 있다. 여기에서 request() 가 조건에 만족하므로 프록시를 생성한다.
만약 조건에 맞는 것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않는다.
2. 어드바이스 적용 여부 판단 - 사용 단계
프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다. 앞서 설명한 예에서 orderControllerV1 은 이미 프록시가 걸려있다.
orderControllerV1 의 request() 는 현재 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고, target 을 호출한다.
orderControllerV1 의 noLog() 는 현재 포인트컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 바로 target 만 호출한다.
참고
프록시를 모든 곳에 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다. 그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.
그런데 위에서 어플리케이션을 실행하면 우리가 적용하길 우너치 않은 곳에서도 프록시가 적용되어 로그가 남게된다.
//얘네 뭐지?
[e3b986cd] AppV1Config.orderControllerV1()
[e3b986cd] |-->AppV1Config.orderServiceV1()
[e3b986cd] | |-->AppV1Config.orderRepositoryV1()
[e3b986cd] | |<--AppV1Config.orderRepositoryV1() time=0ms
[e3b986cd] |<--AppV1Config.orderServiceV1() time=4ms
[e3b986cd] AppV1Config.orderControllerV1() time=13ms
[4dfc9d43] AppV2Config.orderControllerV2()
[4dfc9d43] |-->AppV2Config.orderServiceV2()
[4dfc9d43] | |-->AppV2Config.orderRepositoryV2()
[4dfc9d43] | |<--AppV2Config.orderRepositoryV2() time=1ms
[4dfc9d43] |<--AppV2Config.orderServiceV2() time=3ms
[4dfc9d43] AppV2Config.orderControllerV2() time=8ms
[52ac6146] EnableWebMvcConfiguration.requestMappingHandlerAdapter()
[52ac6146] EnableWebMvcConfiguration.requestMappingHandlerAdapter() time=53ms
[b77e1371] EnableWebMvcConfiguration.requestMappingHandlerMapping()
[b77e1371] EnableWebMvcConfiguration.requestMappingHandlerMapping() time=5ms
잘 보면 AppV1Config.orderControllerV1(), EnableWebMvcConfiguration.requestMappingHandlerAdapter() 이러한 빈 객체와 메서드들도 포인트컷에서 정한 조건에 해당되어 프록시가 적용된 것이다.
결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에 'request' 또는 'order'라는 단어만 들어가 있으면 프록시가 만들어지게 되고, 어드바이스도 적용되는 것이다.
즉 우리는 패키지에 메서드 이름까지 함께 적용할 수 있는 매우 정밀한 포인트 컷이 필요하다.
그것이 바로
AspectJExpressionPointcut
AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다. AspectJ 포인트컷 표현식과 AOP는 조금 뒤에 자세히 설명하겠다. 포인트컷의 표현식에 대해서는 나중에 알아보자.
@Slf4j
@Import({AppV1Config.class, AppV2Config.class})
@Configuration
public class AutoProxyConfig {
@Bean
public Advisor advisor2(LogTrace logTrace) {
// ---- pointcut ----
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
//hello.proxy.app패키지와 그 하위의 모든 패키지 && noLog메서드 제외
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(*hello.proxy.app..noLog(..))");
// ---- advice ----
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
이렇게 하면 hello.proxy.app 패키지와 그 하위에 적용되고, 다만 noLog메서드는 제외한다.
이제 우리가 적용하길 원치 않는 메소드에는 프록시가 적용되지 않는다.
이제 하나의 프록시에 여러 Advisor를 적용해보자.
어떤 스프링 빈이 advisor1 , advisor2 가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 하나만 생성하고, 해당 프록시에 여러 개의 어드바이저를 넣는다.
![](https://blog.kakaocdn.net/dn/etXNGH/btrqwuOD7R6/9IUnbmn4Wl8wLBfPlofhnk/img.png)
프록시 자동 생성기 상황별 정리 :
1. advisor1 의 포인트컷만 만족 프록시1개 생성, 프록시에 advisor1 만 포함
2. advisor1 , advisor2 의 포인트컷을 모두 만족 프록시1개 생성, 프록시에 advisor1 , advisor2 모두 포함
3. advisor1 , advisor2 의 포인트컷을 모두 만족하지 않음 프록시가 생성되지 않음
자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 덕분에 개발자는 매우 편리하게 프록시를 적용할 수 있다. 이제 Advisor 만 스프링 빈으로 등록하면 된다.
다음에는 @Aspect 어노테이션을 사용하여 더 쉽게 어드바이저를 등록해보자.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어 2 - AOP 개념 (0) | 2022.01.12 |
---|---|
스프링 코어 2 - @Aspect AOP (0) | 2022.01.12 |
스프링 코어 2 - 빈 후처리기 (0) | 2022.01.11 |
스프링 코어 2 - 포인트 컷, 어드바이스, 어드바이저 (0) | 2022.01.10 |
스프링 코어 2 - 스프링이 지원하는 프록시(프록시 팩토리) (0) | 2022.01.10 |