스프링 AOP : Bean PostProcessor를 이용한 의존관계 주입
- Spring/Spring
- 2022. 1. 29.
이 포스팅은 인프런의 김영한님 강의를 복습하며 정리한 글입니다.
프록시 팩토리의 한계
앞선 글(https://ojt90902.tistory.com/701)에서 프록시 팩토리만으로는 @Component로 등록되는 스프링 빈들을 프록시 형태로 처리 해줄 수 없다는 한계점이 있다는 것을 알았다. 덤으로 의존관계 주입도 하나하나 @Configuration에서 해야하는데, 이 설정도 무척이나 번거롭다.
그렇다면 프록시 팩토리만 사용했을 때의 위 한계점은 어떻게 극복할 수 있을까?
빈 후처리기(Bean PostProcessor)의 활용
기본적으로 스프링 빈은 위의 그림처럼 등록이 된다. @Bean, @Component, @Import 어노테이션이 붙은 클래스들을 대상으로 스프링은 빈을 생성해서 스프링 빈 저장소에 자동으로 등록을 해준다. 이후에는 스프링의 빈 저장소에서 빈을 검색해서 의존관계 주입 등으로 사용한다.
스프링 빈 후처리기(Bean PostProcessor)는 스프링이 객체를 생성해서 스프링 빈 저장소에 저장하기 직전에 작용을 하는 녀석이다. 실제 동작과정은 다음과 같이 동작한다.
- 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다(@Bean, @Import, @Component 등)
- 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달해준다.
- 후 처리 작업 : 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기하거나, 그대로 전달한다
- 등록 : 빈 후처리기는 빈을 반환한다. 반환된 빈은 스프링 빈 저장소에 저장된다.
실제 코드 동작 결과에서도 알 수 있다. BeanPostProcessorChecker가 BeanPostProcessor가 있는지 확인하고, BeanPostProcessor가 있으면 빈 후처리기로 생성된 빈을 넘겨주는 것을 볼 수 있다.
빈 후처리기(Bean PostProcessor) 사용 방법
빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현해서 스프링 빈으로 등록을 해주면 된다. 인터페이스를 구현하면 아래 두 가지 메서드 중 하나를 선택해서 구현하면 된다.
- postProcessBeforeInitialization : 빈 후처리기가 객체 생성 이후 @PostConsturct 같은 초기화가 발생하기 전에 호출됨.
- postProcessAfterInitiatiozation : 빈 후처리기가 객체 생성 이후 @PostConsturct 같은 초기화가 발생 후에 호출됨
빈 후처리기가 스프링 빈으로 등록되면, 스프링은 모든 빈을 등록하기 직전에 모든 빈 후처리기를 한번씩 다 통과하게 해준다. 이것이 의미하는 것은 모든 빈이 통과하기 때문에 모든 빈을 변경할 것이 아니라면, 적절한 필터링이 필요하다는 것이다.
참고 : @PostConsturct의 비밀
@PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화는 단순히 @PostConstruct 에노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 더 쉽게 이야기 하면 생성된 빈을 한번 조작한다. 그렇다면 @PostConstruct는 어떻게 빈을 조작할까?
스프링은 CommonAnnotationBeanPostPrcessor라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 어노테이션이 붙은 메서드를 호출한다.
빈 후처리기 테스트 코드로 간단히 알아보기
빈 후처리기 사용하지 않을 때
public class BasicTest {
@Test
void noPostProcessor() {
AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(BasicConfig.class);
classA a = context.getBean("A", classA.class);
a.call();
}
@Configuration
static class BasicConfig{
@Bean
public classA A() {
return new classA();
}
}
static class classA{
public void call(){
log.info("callA");
}
}
static class classB{
public void call(){
log.info("callB");
}
}
}
- 스프링 빈으로 classA()를 만들어서 빈 저장소에 등록했다.
- 테스트 코드 내에서는 스프링 빈 저장소에서 빈 이름으로 값을 찾아와서, call() 함수를 실행했다.
- 실행 결과는 classA의 call() 결과인 "callA"가 나온다.
빈 후처리기를 사용할 때
빈 후처리기를 사용하기 위해서는 BeanPostProcessor 인터페이스를 구현하고, 빈으로 등록해주면 된다.
public class BeanPostPrcoessorTest {
@Test
void beanPostProcessorTest() {
AnnotationConfigApplicationContext context = new
AnnotationConfigApplicationContext(BasicConfig.class);
classB a = (classB) context.getBean("A");
a.call();
}
@Configuration
static class BasicConfig{
@Bean
public classA A() {
return new classA();
}
@Bean
public Postprocessor postprocessor() {
return new Postprocessor();
}
}
static class Postprocessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return new classB();
}
}
static class classA{
public void call(){
log.info("callA");
}
}
static class classB{
public void call(){
log.info("callB");
}
}
}
- 빈 포스트 프로세서를 구현했고, 빈 생성 + 초기화 이후에 등록된 빈들이 classB로 바꾸어 등록되도록 코드를 작성했다.
- @Bean으로 등록하는 것은 classA()다.
- classA()는 생성되어서 등록되는 과정에 빈 포스트 프로세서로 들어간다.
- 빈 포스트 프로세서는 classB()를 반환해준다.
- 따라서 A라는 스프링 빈에 저장된 객체는 classB()고 실제 call() 메서드 호출 결과는 "callB"가 나오게 된다.
실제로 동작되는 것은 다음과 같이 된 것으로 이해할 수 있다.
실제 코드에 적용하기
실제 코드에 적용하기 위해서는 위에서 했던 것과 동일한 일을 해야한다. BeanPostProcessor의 구현체 구현한다. 그리고 구현체를 스프링 빈으로 등록해준다. 스프링 빈으로 등록해주면, 스프링 부트가 뜰 때 BeanPostProcessor 체커가 확인 후, 해당 빈 포스트 프로세서에 빈을 전달해준다.
Bean PostProcessor 구현
@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackageName;
private final Advisor advisor;
public PackageLogTracePostProcessor(String basePackageName, Advisor advisor) {
this.basePackageName = basePackageName;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
String baseName = bean.getClass().getPackage().getName();
// 적용하지 않은 패키지 필터링
if (!baseName.startsWith(basePackageName)) {
return bean;
}
// 모두 동적 프록시로 만들어짐.
// 포인트컷으로 특정 메서드만 적용
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
log.info("프록시가 만들어졌습니다. beanName = {}, 프록시 = {}", beanName, proxy.getClass());
return bean;
}
}
- 모든 빈은 등록될 때, 빈 후처리기를 통과한다.
- 모든 빈에 어드바이저를 등록할 필요는 없다. 따라서 등록하고 싶지 않으면 return bean 처리를 해준다.
- 등록이 필요한 빈은 프록시 객체를 만들어 프록시 객체로 바꿔치기 해준다.
이 때 구분해서 알아둬야 할 부분은 빈 포스트 프로세서는 빈을 바꿔치기 해주는 용도다. 포인트컷은 등록된 빈에서 어떤 메서드가 실행될 때만 어드바이스를 적용 할 지를 결정한다.
- 빈 포스트 프로세서 : 빈 객체를 바꿔치기 해줌
- 포인트컷 : 프록시 빈 객체에 등록된 필터링 기능임. 프록시 빈 객체가 소환되고 동적으로 메서드가 넘어왔을 때, 이 메서드가 이 클래스에서 실행되어도 될지 안될지를 결정해줌.
위의 개념을 명확히 구별해야한다.
Bean PostProcessor의 스프링 빈 등록
@Import({AppV1Config.class, AppV2Config.class})
@Configuration
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor packageLogTracePostProcessor(LogTrace logTrace) {
Advisor advisor = getAdvisor(logTrace);
return new PackageLogTracePostProcessor("hello.proxy.app", advisor);
}
private Advisor getAdvisor(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- @Configuration을 통해 빈 후처리기를 등록할 수 있다.
- 빈 후처리기는 프록시 객체로 바꿔주고, 프록시 객체에 어드바이저를 이용해서 넘겨주기 때문에 어드바이저를 넘겨줬다.
- 프록시 객체에 등록된 포인트 컷은 모든 메서드에 동작할 수 있도록 AspectJExpressionPointCut을 사용했다.
실행 결과
스프링 부트를 띄워보면, 다음과 같이 빈 포스트 프로세서를 통과하면서 원하는 빈 객체들이 프록시 객체로 변경되어 스프링 빈 저장소에 저장되는 것을 알 수 있다. 그렇다면 어떻게 각 스프링 빈들의 프록시 객체 의존관계가 형성되는 것일까?
스프링 빈 의존관계 설정
각 객체들의 의존관계가 설정되는 것은 위의 @Configuartion 덕분이다.
예를 들어 orderRepositoryV1이라는 이름의 스프링빈이 스프링 빈 저장소에 저장된다고 해보자. 이 때, 초기에 스프링 빈 객체로 생성되는 것은 OrderRepositoryV1Impl 타입의 인스턴스다. 그런데 이 객체는 스프링 빈 저장소에 저장되기 직전에 빈 포스트 프로세서를 통과하면서 내부적으로 OrderRepositoryV1Impl을 가지는 프록시 객체가 스프링 빈으로 등록된다.
orderServiceV1은 내부적으로 orderRepositoryV1()의 결과로 받는 빈의 의존관계를 주입 받는다. 따라서 orderServiceV1Impl은 orderRepositoryV1()의 결과인 빈 프록시 객체를 내부 참조한다. 그리고 이 객체는 스프링 빈 저장소에 저장되기 전에 빈 후처리기에 들어가면서 다시 한번 빈 프록시 객체로 변경된다.
@ComponentScan의 대상도 유사하게 동작한다. OrderRepositoryV3가 빈 저장소에 등록될 때, 빈 후처리기를 통해 orderRepositoryV3이라는 빈 이름으로 OrderRepositoryV3의 인스턴스 참조를 가지는 프록시 객체가 저장된다.
이후 OrderServiceV3가 빈 저장소에 등록될 때, @AutoWired를 통해 OrderRepositoryV3의 이름을 가지는 스프링 빈 저장소에서 값을 주입받는다. 이 때, 이름으로 검색을 하게 되는데 OrderRepositoryV3 이름을 가지는 빈은 실제로는 프록시 객체이기 때문에 프록시 객체를 참조로 가지게 된다.
빈 후처리기 정리
프록시 팩토리만 사용했을 때의 문제점은 2개였다.
- 너무 많은 의존관계 설정을 개발자가 직접 해야한다.
- 컴포넌트 스캔의 대상은 프록시 팩토리를 직접적으로 이용할 수 없었다.
1번은 그렇다 치더라도, 2번은 정말 방법이 없었다. 왜냐하면 이미 @Component에 의해서 스프링 빈으로 자동으로 등록된 스프링 빈을 프록시 팩토리를 통해서 등록할 수 있는 방법이 없었기 때문이다. 그런데 이런 문제는 빈 후처리기를 이용하면서 처리가 가능했다.
스프링은 빈을 등록할 때, Bean PostProcessor를 통해서 빈 후처리기가 스프링 빈 저장소에 있는지 확인하고, 있을 경우 모든 스프링 빈이 등록되기 전에 Bean PostProcessor를 통과하도록 설정한다. 따라서, @Component, @Bean, @Import 등의 스프링 빈이 등록될 때 반드시 이 빈 후처리기를 통과하기 때문에 프록시 객체 생성이 필요한 것들을 Bean PostProcessor에서 적절하게 변경될 수 있도록 해주면 되었다.
그런데 개발자의 욕심은 끝이 없다. 이 빈 후처리기를 생성하는 로직마저도 비슷비슷한 거 같으니, 누가 해줬으면 좋겠다는 마음을 가지게 된다. 그 마음은 스프링이 들어준다. 스프링 AOP는 빈 후처리기마저 자동으로 제공해준다. 다음 포스팅에서는 스프링이 제공해주는 빈 후처리기에 대해 알아보고자 한다.
'Spring > Spring' 카테고리의 다른 글
Spring AOP : @Aspect를 이용한 프록시 객체 실습 (0) | 2022.02.02 |
---|---|
스프링 AOP : 스프링이 제공하는 빈 후처리기, AutoProxyCreator (1) | 2022.01.29 |
스프링 AOP : ProxyFactory를 이용한 동적 프록시 처리 (0) | 2022.01.29 |
스프링 AOP : 동적 프록시 적용(JDK 동적 프록시, CGLIB) (0) | 2022.01.28 |
스프링 AOP : 프록시 패턴, 데코레이터 패턴 도입 (0) | 2022.01.28 |