지난 포스팅에서는 프록시 팩토리에 대해서 알아보았다.
프록시 팩토리에 어드바이저를 설정해주는 것만으로 프록시를 생성할 수 있게 되었다.
하지만 프록시 팩토리에도 단점이 있는데,
이번 포스팅에서는 이런 문제를 해결할 수 있는 빈 후처리기에 대해 알아보도록 하자.
스프링에서는 @Bean이나 @Component로 스프링 빈을 등록하고 이를 컨테이너에 저장한다.
빈 후처리기는 이 객체를 컨테이너에 저장하기 전에 조작하는 기능을 제공한다.
바로 코드로 확인해보자.
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.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean("beanB"));
}
// 의존성 주입
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
// beanA만 스프링 빈으로 등록
@Bean(name = "beanA")
public A a() {
return new A();
}
// 빈 후처리기를 스프링 빈으로 등록
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
// 예제 클래스 정의
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B{
public void helloB() {
log.info("hello B");
}
}
// 빈 후처리기
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
// 만약 빈이 A의 인스턴스라면 B의 인스턴스를 반환
if (bean instanceof A) {
return new B();
}
return bean;
}
}
}
빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하면 되는데, BeanPostProcessor 아래의 두 메서드를 디폴트 메서드로 정의하고 있다.
객체가 생성되고 난 후 객체를 초기화할 때 사용하는 어노테이션이다.
초기화는 한번만 하면 되므로 @PostConstruct는 한번 수행이되고 만다. 그런데 생각해보면 앞서 확인한 빈 후처리기가 이것과 유사한 역할을 한다.
@PostConstruct는 빈후처리기를 이용한 기능인데, 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록해 여기서 @PostConstruct이 붙은 메서드를 호출하는 것이다.
그렇다면 빈 후처리기를 이용한 로그 추적기의 코드는 어떻게 변할까?
@Slf4j
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("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;
}
}
이제 필요한 객체를 빈을 등록해주자.
@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) {
// pointcut
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
// advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
이제 어플리케이션을 실행해보면
인터페이스를 이용한 컨트롤러는 JDK동적 프록시로
구현 클래스를 이용한 컨트롤러는 CGLIB로
컴포넌트 스캔을 이용한 컨트롤러는 CGLIB로
구현 기술에 관계없이 프록시를 생성한 것을 확인할 수 있다.
빈 후처리기를 생성해 사용하는 것만으로도 많은 로직이 줄어든 것을 확인할 수 있는데, 스프링에서 제공하는 빈 후처리기를 이용하면 더 쉽게 프록시를 적용할 수 있다.
먼저 라이브러리를 추가해주자.
implementation 'org.springframework.boot:spring-boot-starter-aop'
이 라이브러리를 추가하면 스프링 부트는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 빈으로 등록한다.
이름 그대로 스프링 빈으로 등록된 Advisor를 찾아서 프록시를 적용해준다.
자동 프록시 생성기의 동작 과정은 다음과 같다.
그럼 코드가 어떻게 바뀌는지 확인해보자.
@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);
}
}
앞선 포스팅에서 포인트컷의 종류에 대해 알아볼 때 가장 자주 쓰이는 것은 AspectJExpressionPointcut이라고 하였는데, 이 포인트컷을 이용하면 훨씬 정교한 포인트컷을 적용할 수 있다. 이건 다음에 자세하게 알아보자.
하나의 타겟에 조건을 만족하는 Advisor가 2개 이상이라면 프록시는 몇개가 생성이 될까?
프록시는 하나만 생성을 한다.
빈 후처리기는 컨테이너에 등록된 모든 어드바이저와 빈의 모든 정보를 매칭하여 하나의 조건이라도 부합하면 프록시를 생성한다.
이렇게 프록시 팩토리가 생성하는 프록시는 내부에 여러 개의 Advisor를 가질 수 있다.
따라서 포인트컷이 하나라도 만족하지 않으면 프록시를 생성하지 않고,
하나라도 만족한다면 프록시를 생성,
여러 어드바이저의 포인트컷을 만족한다면 하나의 프록시에 포인트컷에 만족하는 여러 어드바이저를 포함하게 된다.
이것으로 빈 후처리기에 대해 알아보았다.
빈 후처리기는 스프링 빈을 생성하고, 컨테이너에 등록하는 과정 중간에서 생성된 객체를 조작하는 기능을 제공한다.
빈 후처리기를 생성하려면 BeanPostProcessor 인터페이스를 구현하고 빈으로 등록해주면 된다.
스프링은 빈 후처리기를 제공하는데, 라이브러리를 추가하면 스프링에서는 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 자동으로 등록해준다. 덕분에 어드바이저만 등록하면 자동 프록시 생성기에서 빈과 어드바이저를 매칭하여 프록시를 자동으로 생성해준다.
출처 : 김영한 - 스프링 핵심 원리 고급편