이전 포스팅에서 프록시 팩토리를 사용해서 스프링이 지원하는 프록시를 만들고 어드바이저로 부가기능을 생성해보았다.
스프링 빈에서 프록시 객체로 등록하기 위해서 해당 스프링 빈 대신에 프록시 객체를 넣고 프록시 객체 내부에서 부가기능과 함께 실제 객체를 호출하는 식으로 작성을 했다.
이렇게 할 때 문제는 복잡한 빈 등록 설정을 해줘야 한다는 것인데 빈 등록을 할 때 프록시 객체를 넘겨서 설정을 해주는 과정이 번거롭다. 게다가 빈을 컴포넌트 스캔으로 자동 등록할 때에는 이런식으로 수동으로 등록하기가 어렵다.
위와 같은 문제를 해결하기 위해 빈 후처리기를 사용한다.
빈 후처리기 (BeanPostProcessor)
객체를 스프링 빈 저장소에 등록하기 전에 조작을 할 수 있게 해주는 인터페이스다.
객체를 조작하거나 아예 바꿔서 반환할 수 있다.
스프링 빈 등록과정
생성 : 빈 대상이 되는 객체를 생성한다.
전달 : 생성한 객체를 빈 후처리기에 전달한다.
후 처리 작업 : 빈 후처리기에서는 객체를 조작하거나 다른 객체로 변경한다.
등록 : 후 처리 작업이 끝난 객체를 빈 저장소에 등록한다.
위의 등록과정을 살펴보면 후 처리 작업에서 다른 객체로 변경을 하면 해당 객체가 빈 저장소에 등록됨을 확인할 수 있다.
이를 그림으로 살펴보면 다음과 같다.
다른 객체로 바꿔치는 빈 후처리기 예제
먼저 빈 후처리기를 만들어서 빈으로 등록해야 한다.
빈 후처리기를 만들기 위해서는 BeanPostProcessor 인터페이스를 구현해줘야 한다.
BeanPostProcessor는 두 가지 디폴트 메서드가 존재한다.
postProcessBeforeInitialization, postProcessAfterInitialization, 각각 Initialization(@PostConstruct) 전과 후에 적용이 된다고 보면 된다.
다음은 A객체를 B객체로 변경하는 후처리기이다.
static class A{
public void helloA(){
log.info("hello A");
}
}
static class B{
public void helloB(){
log.info("hello B");
}
}
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;
}
}
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a(){
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
위의 코드에서 Config 클래스를 살펴보면 beanA라는 이름으로 A객체를 생성하여 등록하고있다.
하지만 후처리기에서 A객체의 인스턴스이면 B 객체를 생성하여 객체를 변경하고있다.
아래의 테스트코드 결과에서도 볼 수 있듯이 실제로 beanA 로 등록된 객체는 B.class 타입이고 A.class 타입의 객체는 존재하지 않음을 확인할 수 있다.
@Test
void basicConfig(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B는 빈으로 등록된다.
B a = applicationContext.getBean("beanA", B.class);
a.helloB();
//A는 빈으로 등록되지 않는다.
assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}
프록시와 빈 후처리기
다시 한 번 기존의 프록시의 구현 방식을 생각해보자.
빈을 등록할 때 직접 프록시 객체를 생성해서 등록해줬었는데 이를 빈 후처리기를 이용하면 훨씬 간결한 코드로 프록시를 만들 수 있다.
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.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;
}
}
먼저 BeanPostProcessor 를 구현한 클래스를 생성하고 postProcessAfterInitialization 메서드를 재정의한다.
해당 메서드에서는 프록시를 생성해서 반환한다.
위의 코드에서는 패키지를 주입받아서 해당 패키지의 대상일 경우에만 프록시를 생성하여 반환한다.
어드바이저도 주입받아서 프록시를 생성할 때 추가해준다.
다음은 위의 후처리기를 빈으로 등록하는 과정이다.
@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);
}