https://codenme.tistory.com/113
스프링 핵심원리-고급편 5 [스프링에서의 프록시: beanFactory]
https://codenme.tistory.com/111 스프링 핵심원리-고급편 4 [프록시 패턴과 데코레이터 패턴] https://codenme.tistory.com/108 스프링 핵심원리-고급편 3 [스프링 디자인 패턴] https://codenme.tistory.com/105 스프링 핵심
codenme.tistory.com
위의 글에서 이어지는 내용입니다.
@Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
빈 후처리기 - BeanPostProcessor
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
빈 포스트 프로세서( BeanPostProcessor )는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.
빈 후처리기 기능
빈 후처리기의 기능은 막강하다.
객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.
빈 등록 과정을 빈 후처리기와 함께 살펴보자
1. 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
4. 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
빈 후처리기 - 예제 코드1
일반적인 스프링 빈 등록 과정
빈 후처리기를 학습하기 전에 먼저 일반적인 스프링 빈 등록 과정을 코드로 작성해보자.
![](https://blog.kakaocdn.net/dn/5GSks/btskgejSYlu/68kfxl5m1QBceSZaxZEDok/img.png)
A객체만 빈으로 등록하고, B 객체는 빈으로 등록하지 않은 상황을 구현하였다.
public class BasicTest {
@Test
void basicConfig() {
//애플리케이션 컨텍스트(스프링 컨테이너 생성) 생성.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
//A는 빈으로 등록된다.
A a = applicationContext.getBean("beanA", A.class);
a.helloA();
//B는 빈으로 등록되지 않는다, B라는 빈을 가져오면 noBean 에러가 나타난다!
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));
}
@Slf4j
@Configuration
static class BasicConfig { //config에 A만 빈으로 등록!
@Bean(name = "beanA")
public A a() {
return new A();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
}
new AnnotationConfigApplicationContext(BasicConfig.class)
스프링 컨테이너를 생성하면서 BasicConfig.class 를 넘겨주었다. BasicConfig.class 설정 파일은
스프링 빈으로 등록된다.
이제, 빈 후처리기를 통해서, beanA의 A객체를 B객체로 바꿔보자. (hooking)
BeanPostProcessor 인터페이스 - 스프링 제공
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws
BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws
BeansException
}
- 빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
- postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
- postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.
....
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
......
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
if (bean instanceof A) {
return new B();
}
return bean;
}
}
AToBPostProcessor
- 빈 후처리기이다. 인터페이스인 BeanPostProcessor 를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
- 이 빈 후처리기는
if (bean instanceof A)
를 통해 들어온 빈이 A의 인스턴스라는 것을 감지하면, 새로운 B객체로 바꿔치기한다.
파라미터로 넘어오는빈(bean)객체가 A의 인스턴스이면 새로운 B 객체를 생성해서 반환한다.
여기서 A 대신에 반환된 값인 B 가 스프링 컨테이너에 등록된다. 다음 실행결과를 보면 beanName=beanA , bean=A 객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인할 수 있다.
postProcessAfterInitialization: 빈 초기화 이후에, 후킹 처리하기위해 해당 메소드를 오버라이드 했다.
B b = applicationContext.getBean("beanA", B.class)
실행 결과를 보면 최종적으로 "beanA"라는 스프링 빈 이름에 A 객체 대신에 B 객체가 등록된 것을
확인할 수 있다. A는 스프링 빈으로 등록조차 되지 않는다.
정리
빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
이것은 빈 객체를 조작(해당 객체의 특정 메서드를 호출)하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.
@PostConstruct의 비밀
@PostConstruct 는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다.
쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다.
따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 될 것 같다.
스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하는데, 여기에서
@PostConstruct 애노테이션이 붙은 메서드를 호출한다. 따라서 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.
빈 후처리기 - 적용
빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자.
이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
더 나아가서 설정 파일에 있는 수 많은 프록시 생성 코드도 한번에 제거할 수 있다.
ex) 빈에 실제 객체가 등록이 들어오면, 빈 후처리기가 프록시 빈으로 바꿔치기 수행
우선, 특정 패키지 하위의 객체에 빈 후처리기를 통해 프록시로 바꿔치기 해보자.
BeanPostProcessor를 상속받아 빈 후처리기 구현
**주의: 스프링에는 매우많은 빈들이 들어있다.( 내가 만들지 않은) 따라서 패키지 제한을 두지 않으면, 무수히 많은 예상하지 못한 빈들에 프록시가 적용되어 버린다-> 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다.
@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackage; //파라미터로 패키지 제한을 받는다.
private final Advisor advisor; //파라미터로 어드바이저를 받는다.
// pointcut: 프록시 적용 여부 확인 + advice: 프록시 부가기능 )
public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param beanName={} bean={}", beanName, bean.getClass());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 진행
String packageName = bean.getClass().getPackageName();
if (!packageName.startsWith(basePackage)) { //패키지가 맞지 않으면 실제 빈 반환.
return bean;
}
//프록시 대상이면 프록시를 만들어서 반환( 실제 객체를 프록시로 바꿔친다.)
ProxyFactory proxyFactory = new ProxyFactory(bean);// 프록시 팩토리의 파라미터에 빈을 넣어 해당 빈의 프록시 생성
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());
return proxy;
}
}
- PackageLogTraceProxyPostProcessor 는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor 가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다.
- 모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 여기서는 hello.proxy.app 과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.
- 프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다. 따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.
Config
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class}) //V1,V2는 수동 빈 설정파일이라 임포트해야된다.
//하지만, V3는 컴포넌트 스캔 쓰기에 따로 안적어도 된다.
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
//프록시 적용할 패키지 경로 패턴, 어드바이저 전달
return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
}
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); //point cut, advice로 어드바이저 생성하여 반환
}
}
- @Import({AppV1Config.class, AppV2Config.class}) : V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야
동작한다. ProxyApplication 에서 등록해도 되지만 편의상 여기에 등록하자.
- @Bean logTraceProxyPostProcessor() : 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보( hello.proxy.app )와 어드바이저( getAdvisor(logTrace) )를 넘겨준다.
- 이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
위와 같이 구현 후, 실행 시 아래와 같은 결과를 확인할 수 있다.
v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
실행
http://localhost:8080/v1/request?itemId=hello
http://localhost:8080/v2/request?itemId=hello
http://localhost:8080/v3/request?itemId=hello
V1,V2,V3 모두 logTrace가 올바르게 적용되엇다.
이제, 빈이 등록될 때, postProcessAfterInitilizatoin() 메소드를 통해 실제 객체가 프록시로 바꿔치기되고,
이 프록시는 로그 추적이라는 부가기능을 어드바이저(포인트컷 + 어드바이스[실제 부가 기능] ) 로 수행하기에,
간편하게 컴포넌트 스캔, 수동 등록 빈 모두에 적용된다!
컴포넌트 스캔에도 적용
여기서 중요한 포인트는 v1, v2와 같이 수동으로 등록한 빈 뿐만 아니라 컴포넌트 스캔을 통해 등록한 v3 빈들도 프록시를 적용할 수 있다는 점이다. 이것은 모두 빈 후처리기 덕분이다.
프록시 적용 대상 여부 체크
애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다. 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다. 여기서는 간단히 basePackage 를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다.
스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다. 따라서 모든 객체를 프록시로 만들 경우 오류가 발생한다.
빈 후처리기 - 정리
이전에 보았던 문제들이 빈 후처리기를 통해서 어떻게 해결되었는지 정리해보자.
문제1 - 너무 많은 설정
프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1 , ProxyFactoryConfigV2 와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다.
예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다. 무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.
스프링 빈을 편리하게 등록하려고 컴포넌트 스캔까지 사용하는데, 이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 했다.
문제2 - 컴포넌트 스캔
컴포넌트 스캔을 사용하는 경우 beanFactory만으로는 프록시 적용이 불가능했다.
왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
좀 더 풀어서 설명하자면, 지금까지 학습한 방식으로 프록시를 적용하려면, 원본 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라 ProxyFactoryConfigV1 에서 한 것 처럼, 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다. 그런데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.
문제 해결
빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다. 그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
덕분에 애플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.
하지만 개발자의 욕심은 끝이 없다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.
**중요
이번에는 프록시의 적용 대상 여부를 여기서는 간단히 패키지를 기준으로 설정했다.
그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다.
포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다.
뒤에서 학습하겠지만 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
결과적으로 포인트컷은 다음 두 곳에 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
스프링이 제공하는 빈 후처리기1
주의 - 다음을 꼭 추가해주어야 한다. build.gradle - 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'
이 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy 를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. aspectJ 는 뒤에서 설명한다. 스프링 부트가 활성화하는 빈은 AopAutoConfiguration 를 참고하자.
자동 프록시 생성기 - AutoProxyCreator
- 앞서 이야기한 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다(postProcessor).
- 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
- 이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
- Advisor 안에는 Pointcut 과 Advice 가 이미 모두 포함되어 있다. 따라서 Advisor 만 알고 있으면
그 안에있는 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.그리고 Advice로 부가 기능을 적용하면 된다.
참고
AnnotationAwareAspectJAutoProxyCreator(자동 프록시 생성기) 는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서
처리해준다.
Advisor 는 물론이고, @Aspect 도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다. @Aspect 에
대한 자세한 내용은 이후의 글에서 설명하겠다.
![](https://blog.kakaocdn.net/dn/qRxbE/btskfrYxG0S/z2nDBCxy5SdVZrL0yikq80/img.png)
자동 프록시 생성기의 작동 과정을 알아보자
1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 모든 Advisor 빈 조회: 자동 프록시 생성기 - 빈 후처리기는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
4. 프록시 적용 대상 체크: 앞서 조회한 Advisor 에 포함되어 있는 포인트컷을 사용해서 스프링 빈으로 등록되기 전에 빈 후처리기에 전달된 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
5. 프록시 생성: 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.
요약
객체가 빈으로 등록되기 전, 빈 후처리기에 전달되면, 빈 후처리기는 스프링 컨테이너 내 모든 Advisor를 탐색해서,
해당 객체에 만족되는 PointCut을 가진 Advisor를 찾고,
만약 하나도 없으면 실제 객체로, 적용해야하는 pointCut을 가진 Advisor가 있다면,
해당 Advisor의 Advice에 정의된 부가 기능을 사용하는 프록시로 교체해준다.
![](https://blog.kakaocdn.net/dn/mhMvC/btskfqei45v/ENfkrotG08pqlLFXtrlnHk/img.png)
Config
AutoProxyConfig 코드를 보면 advisor1 이라는 어드바이저 하나만 등록했다.
빈 후처리기는 이제 등록하지 않아도 된다. 스프링은 자동 프록시 생성기라는
( AnnotationAwareAspectJAutoProxyCreator ) 빈 후처리기를 자동으로 등록해준다.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
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);
}
}
따라서, 내가 수행하고 싶은 부가기능이 있다면, 어디에 적용할지 조건을 담은 PointCut과 부가 기능을 정의한 Advice를 담은
Advisor만 빈으로 등록하면 된다. ( 스프링 프록시 자동 생성기는 스프링 컨테이너에 등록된 Advisor를 뒤져서 적용하기에, 스프링 컨테이너에 등록되 있지 않으면 당연히 적용도 할 수 없다.)
실행
http://localhost:8080/v1/request?itemId=hello http://localhost:8080/v2/request?itemId=hello http://localhost:8080/v3/request?itemId=hello
실행하면 모두 프록시 적용된 결과가 나오는 것을 확인할 수 있다. 실행하면 로그가 나오면 안됨
http://localhost:8080/v1/no-log
로그가 출력되지 않는 것을 확인할 수 있다.
중요: 포인트컷은 2가지에 사용된다.
1. 프록시 적용 여부 판단 - 생성 단계
자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
클래스 + 메서드 조건을 모두 비교한다. 이때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
예) orderControllerV1 은 request() , noLog() 가 있다. 여기에서 request() 가 조건에 만족하므로 프록시를 생성한다.
만약 조건에 맞는 것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않는다.
2. 어드바이스 적용 여부 판단 - 사용 단계
프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다. 앞서 설명한 예에서 orderControllerV1 은 이미 프록시가 걸려있다.
orderControllerV1 의 request() 는 현재 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고, target 을 호출한다.
orderControllerV1 의 noLog() 는 현재 포인트컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 바로 target 만 호출한다.
스프링이 제공하는 빈 후처리기2
애플리케이션 로딩 로그
EnableWebMvcConfiguration.requestMappingHandlerAdapter()
EnableWebMvcConfiguration.requestMappingHandlerAdapter() time=63ms
애플리케이션 서버를 실행해보면, 스프링이 초기화 되면서 기대하지 않은 이러한 로그들이 올라온다.
그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*" 만 포함되어 있으면 매칭 된다고 판단하기 때문이다.
결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에 request 라는 단어만 들어가 있으면 프록시가 만들어 지고, 어드바이스도 적용되는 것이다.
결론적으로 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointcut
AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다( 실무에서는 거의 이것만 사용한다).
AspectJ 포인트컷 표현식과 AOP는 이후의 글에서 자세히 설명하겠다.
지금은 특별한 표현식으로 복잡한 포인트컷을 만들 수 있구나 라고 대략 이해하면 된다.
@Bean
public Advisor advisor2(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
AspectJExpressionPointcut : AspectJ 포인트컷 표현식을 적용할 수 있다.
execution(* hello.proxy.app..*(..)) : AspectJ가 제공하는 포인트컷 표현식이다. 이후의 글에서 자세히 설명하겠다.
지금은 간단히 알아보자.
* : 모든 반환 타입
hello.proxy.app.. : 해당 패키지와 그 하위 패키지
*(..) : * 모든 메서드 이름, (..) 파라미터는 상관 없음
쉽게 이야기해서 hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다.
이제 위와 같은 패키지 제한에 기존의 포인트컷 제한 조건인
"no log 는 제외" 조건을 추가해보자.
@Bean
public Advisor advisor3(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
표현식을 다음과 같이 수정했다.
execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))
&& : 두 조건을 모두 만족해야 함
! : 반대
hello.proxy.app 패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되, noLog() 메서드는 제외하라는 뜻이다.
실행하면 로그가 나오면 안됨
http://localhost:8080/v1/no-log
이제 로그가 남지 않는 것을 확인할 수 있다.
하나의 프록시, 여러 Advisor 적용
만약 어떤 스프링 빈이 advisor1 , advisor2 가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까?
프록시 자동 생성기는 프록시를 하나만 생성한다. 왜냐하면 프록시 팩토리가 생성하는 프록시는 내부에 여러 advisor 들을 포함할 수 있기 때문이다. 따라서 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다.
프록시 자동 생성기 상황별 정리
advisor1 의 포인트컷만 만족 프록시1개 생성, 프록시에 advisor1 만 포함
advisor1 , advisor2 의 포인트컷을 모두 만족 프록시1개 생성, 프록시에 advisor1 , advisor2 모두 포함
advisor1 , advisor2 의 포인트컷을 모두 만족하지 않음 프록시가 생성되지 않음
다음 글에 설명할 스프링 AOP도 동일한 방식으로 동작한다.
정리
자동 프록시 생성기인 AnnotationAwareAspectJAutoProxyCreator 덕분에 개발자는 매우 편리하게 프록시를 적용할 수 있다.
이제 Advisor 만 스프링 빈으로 등록하면 된다.
Advisor = Pointcut + Advice
다음 글에서는 @Aspect 애노테이션을 사용해서 더 편리하게 포인트컷과 어드바이스를 만들고 프록시를 적용해보자.
+중요
스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
결과적으로 포인트컷은 다음 두 곳에 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
스프링의 AOP를 처음 공부하거나 사용하면, AOP 적용 수 만큼 프록시가 생성된다고 착각하게 된다.
(굳이 advisor, AOP가 추가된다고 1대1 매칭으로 프록시가 같이 늘어날 필요 없이 하나의 프록시에서 다수의 advisor, AOP를 관리할 수 있다.)
- 스프링은 AOP를 적용할 때, 최적화를 진행해서 지금처럼 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
- 정리하면 하나의 target 에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target 마다 하나의 프록시만 생성한다. 이부분을 꼭 기억해두자
'Spring boot' 카테고리의 다른 글
스프링 핵심원리 9 [스프링 AOP] (0) | 2023.06.17 |
---|---|
스프링 핵심원리 8 [@Aspect AOP] (1) | 2023.06.17 |
스프링 핵심원리 6 [스프링에서의 프록시: proxyFactory] (0) | 2023.06.16 |
스프링 핵심원리 5 [동적 프록시 기술] (0) | 2023.06.16 |
스프링 핵심원리 4 [프록시 패턴과 데코레이터 패턴] (0) | 2023.06.15 |