1. 빈 후처리기?
일반적인 상황에서는 컴포넌트 스캔을 통해 스프링 빈을 등록하게 되면 실제 객체가 등록된다. 당연한 것이다.
Configuration 파일을 통해 직접 수동으로 프록시 객체를 등록하지 않는 이상 스프링 컨테이너에는 실제 객체가 등록된다.
이러한 상황에서, 빈 후처리기라는 것은 스프링 컨테이너에 실제 스프링 빈을 등록하기 전 다양한 작업을 할 수 있다.
- 객체를 조작
- 완전히 다른 객체로 바꿔치기도 가능
이러한 빈 후처리기를 이용한다면, Configuration 파일의 모든 빈에 하나하나 프록시 설정을 해줬던 모습을 지울 수 있지 않을까?
혹은 컴포넌트 스캔을 통해 자동으로 등록하는 스프링 빈에 대해서 프록시로 바꿔치기할 수 있지 않을까?
직접 알아보자!
먼저 빈 후처리기는 `BeanPostProcessor`인터페이스를 구현해 만들 수 있다.
오버라이딩 할 메서드는 두 가지가 있는데, 이는 각각 @PostConstruct가 호출되기 전 후에 실행할 로직을 의미한다.
본 예제에서는 스프링 빈이 완전히 초기화된 후인 `postProcessAfterInitialization()`메서드를 활용했다.
@Slf4j
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;
}
}
만약 기존의 원본 스프링 빈이 A 타입의 클래스라면 *B타입으로 객체를 바꿔치기하는 빈 후처리기이다.*
실제 테스트 코드를 확인해보자.
public class BeanPostProcessorTest {
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B 객체가 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
//A는 등록되지 않았다.
Assertions.assertThatThrownBy(() -> applicationContext.getBean(A.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
}
Configuration 파일에서 직접 만든 빈 후처리기를 스프링 빈으로 등록해준 뒤, A 클래스 타입의 스프링 빈을 등록했다.
테스트 결과 역시 원하는 결과를 얻을 수 있었다.
@PostConstruct와 조금 헷갈리는 부분이 있었다. 해당 애노테이션의 경우 스프링 빈이 생성될 때 초기화시켜줄 메서드를 지정하는 기능이다.
빈 후처리기도 실제로 스프링 빈으로 등록될 때 뭔가를 처리해준다는 관점에서 "비슷한 기능이 아닌가..?"라는 의문을 가지게 되었다.
결론적으로 @PostConstruct 애노테이션은 개별 스프링 빈에 대해 데이터 베이스 커넥션 초기화, 혹은 네트워크 연결 초기화 등 애플리케이션 환경을 초기화하는 데 사용되며,
빈 후처리기는 모든 스프링 빈에 대해 공통적으로 적용될 초기화 로직을 적용할 때 사용하거나 스프링 빈을 동적으로 변경하는 경우 사용한다고한다. 스프링 빈을 동적으로 변경하는 경우가 아마 프록시를 의미하겠지?
추가로 스프링은 `CommonAnotationBeanPostProcessor`라는 빈 후처리기를 자동으로 등록하는데, 여기에서 바로 @PostConstruct 애노테이션이 붙은 메소드를 전부 호출해준다. 스프링 스스로도 내부 기능을 위해 빈 후처리기를 사용하는 것이다.
2. 적용
실제 애플리케이션에 이제 빈 후처리기를 적용해보자.
먼저 빈 후처리기를 만들자.
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={}, bean=()", beanName, bean);
//프록시 적용 대상 여부 체크, 아니면 원본 반환
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;
}
}
- 해당 클래스는 프록시 팩토리에 어드바이저를 주입하기위해 내부 필드에 Advisor를 가진다. (외부에서 주입)
- 프록시 적용 대상 여부를 체크하기 위해 basePackage를 내부 필드로 가지고, 필터링하는 로직에 사용한다.
- 왜 basePackage를 가져야할까?
- 빈 후처리기는 별도의 필터링이 없으면 스프링 컨테이너에 등록된 모든 스프링 빈에 대해서 로직을 수행한다.
- 우리가 프록시를 등록하고 싶은 대상 외에도, 스프링이 자동으로 등록하는 스프링 빈들이 존재하기 때문에 basePackage를 설정했다.
- 중요한 부분이다!
- 프록시 적용 대상 여부에 속한다면 `ProxyFactory`를 생성해 어드바이저를 주입하고 프록시 객체를 반환한다.
이제 빈 후처리기를 스프링 빈으로 등록하자.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
}
private Advisor getAdvisor(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- 어드바이저를 외부에서 주입해주는 방식으로 빈 후처리기를 만들었기 때문에 생성 후 주입해주는 모습이다.
- @Import를 통해 수동으로 스프링 빈을 등록할 Configuration 클래스들을 지정해주었다.
이렇게만 하면 설정이 끝난다...
이제 Configuration 파일에 더 이상 프록시 관련 코드들이 덕지덕지 붙어있지 않다!
추가로 컴포넌트 스캔을 통해 자동으로 등록한 스프링 빈에 대해서도 프록시가 적용된다.
3. 포인트컷의 활용
위에서 프록시 적용 대상 여부를 간단히 basePackage를 만들어 확인했다.
그런데 생각해보면, 이미 어드바이저를 갖고있으니까 포인트컷을 활용하면 더 깔끔하지 않겠는가?
다음에는 실제로 스프링이 제공하는 빈 후처리기를 활용하며 *적용 대상 여부 판단에도 포인트컷을 활용할 것이다.*
결과적으로 포인트컷은 두 곳에서 사용된다는 것이다. 이 부분은 중요하다! 후술할 포스팅에서도 한번 더 다룰 예정이다.
- 프록시의 어떤 메서드가 호출되었을 때 어드바이스를 적용할지 말지 판단(프록시 내부에서)
- 프록시 객체 자체를 만들지 말지 빈 후처리기가 확인하는 경우, 모든 어드바이저 내의 포인트컷을 하나하나 가져와 하나라도 적용될 여지가 있으면 일단 프록시 객체로 생성한다. (빈 후처리기 - 자동 프록시 생성)
출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편
'Spring' 카테고리의 다른 글
[Spring] @Aspect (0) | 2024.03.07 |
---|---|
[Spring] AutoProxyCreator (스프링이 제공하는 빈 후처리기) (2) | 2024.03.06 |
[Spring] 동적 프록시의 추상화, ProxyFactory (1) | 2024.03.06 |
[Spring] 동적 프록시(JDK Dynamic Proxy, CGLIB) (2) | 2024.03.05 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2024.03.04 |