동적 프록시를 위한 빈 후처리기
빈 후처리기(BeanPostProcessor)
스프링은 @Bean이나 컴포넌트 스캔을 통해 대상 객체를 생성하고 스프링 컨테이너의 빈 저장소에 빈으로 등록한다.
빈 후처리기(BeanPostProcessor)는 빈으로 등록할 목적으로 생성된 객체를 빈 저장소에 등록하기 전에 조작하는 기능이다. 빈 후처리기는 생성된 객체를 조작할 수도 있으며, 완전히 다른 객체로 변경하는 것도 가능하다.
빈 후처리기를 포함한 빈 등록 과정은 다음과 같다.
- 빈으로 등록될 대상 객체를 생성한다(@Bean, 컴포넌트 스캔).
- 생성된 객체를 컨테이너의 빈 저장소에 등록하기 전에 빈 후처리기에 전달한다.
- 빈 후처리기는 작성된 로직에 따라 객체를 조작한다.
- 빈 후처리기는 작업이 끝나면 조작된 객체를 반환한다. 반환된 객체는 빈 저장소에 등록된다.
예제 코드
예제 코드를 통해 빈 후처리기를 어떻게 사용할 수 있는지 확인해보자.
스프링 빈 등록 대상
A, B는 스프링 빈으로 등록될 대상들이다.
각각 helloA()와 helloB()를 구현하고 있으며, 해당 메소드들은 호출되는 것을 알리는 로그를 찍는다.
public class BeanPostProcessorTest {
...
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
...
}
빈 후처리기
빈 후처리기는 BeanPostProcessor를 구현하고 스프링 빈으로 등록하여 사용할 수 있다.
AToBPostProcessor는 이번 예제에서 사용할 빈 후처리기이다.
빈 후처리기의 로직은 postProcessBeforeInitialization() 또는 PostProcessAfterInitialization()에 작성할 수 있다.
각각 초기화가 발생하기 전, 초기화가 발생한 후에 호출되는 메서드이다.
이번 예제에서는 PostProcessAfterInitialization()에 로직을 작성할 것이다.
로직은 만약 빈으로 등록될 대상이 A 객체라면, B 객체를 반환하는 로직이다.
public class BeanPostProcessorTest {
...
// 빈후처리기
// BeanPostProcessor의 구현체로 구현한다.
@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;
}
}
}
설정 클래스
BeanPostProcessorConfig는 이번 예제에서 사용할 설정 클래스이다.
beanA라는 빈 이름으로 A 객체를 등록하도록 되어있다.
그리고 빈 후처리기인 AToBPostProcessor 객체를 등록하도록 되어있다.
public class BeanPostProcessorTest {
...
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
// A 객체를 생성한다.
// 빈후처리기가 있으므로, 빈 이름과 A 객체를 빈후처리기에 전달한다.
@Bean(name = "beanA")
public A a() {
return new A();
}
// 빈후처리기는 A 객체를 전달받는다.
// 빈후처리기 로직에 따라 A 객체를 B 객체로 변경한다.
// 빈 이름이 beanA인 B 객체를 빈으로 등록한다.
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
...
}
실제 사용
beanPostProcessorConfig()에는 실제로 어떻게 빈 후처리기가 작동하고, 어떤 결과가 발생하는지 작성되어있다.
BeanPostProcessorConfig.class를 파라미터로 주입 받는 ApplicationContext applicationContext를 생성한다.
applicationContext는 스프링 컨테이너이며, BeanPostProcessorConfig에 기반하여 스프링 빈이 등록되어있다.
applicationContext.getBean("beanA", B.class)를 통해 빈 이름이 beanA이고, 대상 객체가 B 객체인 스프링 빈을 조회할 수 있다. 이런 결과가 나오게 된 과정은 아래와 같다.
- beanA 이름으로 등록될 A 객체를 생성한다.
- 빈 후처리기가 있으므로 빈 이름과 A 객체를 빈 후처리기에 전달한다.
- 빈 후처리기는 로직에 따라 A 객체를 B 객체로 변경한다.
- beanA 이름으로 B 객체가 빈 저장소에 등록된다.
public class BeanPostProcessorTest {
@Test
void beanPostProcessorConfig() {
// 스프링 컨테이너
// BeanPostProcessorConfig를 토대로 스프링 컨테이너에 스프링 빈을 등록한다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(
BeanPostProcessorConfig.class);
// beanA의 객체가 A에서 B로 변경되어 빈으로 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB(); // B - hello B
// A는 빈으로 등록되지 않는다.
Assertions.assertThatThrownBy(() -> applicationContext.getBean(A.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
}
...
}
프록시를 위한 빈 후처리기 사용
스프링 빈을 등록하기 전에 대상 객체를 조작할 수 있는 빈 후처리기는 프록시를 위해 사용될 수 있다.
예를 들어 컴포넌트 스캔으로 등록되는 객체들은 중간에 조작할 방법이 없는데, 빈 후처리기를 이용하면 컴포넌트 스캔과 @Bean으로 등록되는 모든 객체들을 빈 저장소에 등록하기 전에 조작할 수 있다. 즉 실제 객체를 프록시 객체로 변경하여 등록할 수 있다는 것이다.
또 하나 중요한 점은 빈을 등록할 때 마다 프록시 생성을 위한 코드를 매번 작성할 필요가 없어진다는 것이다. 프록시 생성을 위한 로직은 빈 후처리기에서 통합적으로 관리할 수 있기 때문이다.
예제 코드를 통해 빈 후처리기가 프록시를 위해 어떻게 사용될 수 있는지 알아보자.
빈 후처리기
PackageLogTracePostProcessor는 이번 예제에서 사용할 빈 후처리기 이다.
String basePackage, Advisor advisor를 생성사로 주입 받는다.
basePackage는 프록시 적용 여부를 판단할 기준이다. 해당 패키지 하위 대상에 대해 프록시 객체를 생성할 것이다.
advisor는 프록시 팩토리에 적용할 어드바이저이다.
postProcessAfterInitialization() 내부를 살펴보자.
빈 후처리기가 전달 받은 객체가 basePackage의 하위 패키지에 위치한 클래스의 객체라면 프록시 팩토리를 통해 프록시 객체를 생성하고 반환한다. 즉 실제 객체가 아닌 프록시 객체가 빈으로 등록된다. 그렇지 않다면 실제 객체를 빈으로 등록한다.
// 빈후처리기
// 원본 객체를 프록시 객체로 변환하는 기능을 제공한다.
@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;
}
}
실제 사용
BeanPostProcessorConfig는 빈 후처리기를 사용하기 위한 설정 파일이다.
basePackage 값과 advisor 객체를 생성자로 주입 받은 PackageLogTracePostProcessor 객체를 빈으로 등록한다.
이제 어플리케이션에서 @Bean 또는 컴포넌트 스캔으로 빈 저장소에 등록될 객체들은 등록되기 전에 빈 후처리기에 전달될 것이다.
그리고 프록시가 적용될 객체라면 실제 객체가 아닌 프록시 객체가 빈으로 등록될 것이고, 그렇지 않다면 실제 객체가 빈으로 등록될 것이다.
// for app.v1~v3
@Slf4j
@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);
}
}
스프링이 제공하는 빈 후처리기
스프링은 이미 다양한 종류의 빈 후처리기를 제공하고 있다.
스프링이 제공하는 빈 후처리기 중 프록시 생성을 위한 빈 후처리기도 있다.
'org.springframework.boot:spring-boot-starter-aop' 라이브러리를 추가하면 aspectjweaver라는 aspectJ 관련 라이브러리가 등록되고, AOP 관련 클래스들이 스프링 빈에 등록된다.
이 때 AnnotationAwareAspectJAutoProxyCreator(=AutoProxyCreator)라는 빈 후처리기도 스프링 빈으로 등록된다.
- 이 빈 후처리기는 자동으로 프록시를 생성해주는 빈 후처리기이다.
- 스프링 빈으로 등록된 모든 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
- 이것이 가능한 이유는 Advisor에 Pointcut이 포함되어 있기 때문이다. 이 Pointcut을 통해 Advice가 어디에 적용되야할 지 알 수 있다. 즉 프록시가 적용되어야 할 대상 객체를 판단할 수 있는 것이다.
AnnotationAwareAspectJAutoProxyCreator의 동작 과정을 단계 별로 알아보자.
- 빈으로 등록될 대상 객체를 생성한다(@Bean, 컴포넌트 스캔).
- 생성된 객체를 컨테이너의 빈 저장소에 등록하기 전에 빈 후처리기에 전달한다.
- 빈 후처리기는 빈으로 등록된 Advisor를 조회한다.
- 조회한 Advisor들에 포함되어 있는 Pointcut을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지를 판단한다. 이 때 객체의 클래스 정보, 메서드 정보를 모든 Pointcut에 모두 매칭해본다. 조건이 하나라도 만족한다면 프록시 적용 대상이 된다.
- 프록시 적용 대상이라면 ProxyFactory를 통해 프록시 객체를 생성하고 반환하고, 아니라면 실제 객체를 그대로 반환한다.
- 반환된 객체가 스프링 빈으로 등록된다.
추가적으로 Pointcut은 2가지에 사용된다는 것을 한번 확인하고 넘어가자.
- Pointcut은 대상 객체에 대한 프록시 적용 여부를 판단하는데 사용된다. 즉 대상 객체의 프록시 객체가 생성될 필요가 있는지 판단하는데 사용된다.
- Pointcut은 어드바이스(프록시 로직) 적용 여부를 판단하는데 사용된다. 즉 프록시 객체를 통해 메소드를 호출할 때 어드바이스가 적용되어야 하는지를 판단한다.
마지막으로 빈으로 등록될 어떤 객체가 다수의 Advisor에 포함된 Pointcut 조건을 만족한다면 AutoProxyCreator는 어떻게 프록시를 생성하는지 집어보고 넘어가자.
이러한 경우에도 프록시는 하나만 생성한다. ProxyFactory로 생성되는 프록시는 내부에 다수의 Advisor를 포함할 수 있기 때문이다.
예제 코드
예제 코드를 통해 AutoProxyCreator를 통해 어떻게 프록시 객체가 생성되고 빈으로 등록되는지 살펴보자.
'org.springframework.boot:spring-boot-starter-aop'가 등록되었고, AutoProxyCreator가 빈으로 등록되어 있는 상황이라는 것을 주의하자.
AutoProxyConfig는 AutoProxyCreator를 통해 프록시 객체를 빈으로 등록하기 위한 설정 파일이다.
advisor3()는 Advisor를 빈으로 등록하는 메소드이다.
AspectJExpressionPointcut pointcut을 Pointcut으로 가지며, LogTraceAdvice advice를 Advice로 가지는 Advisor를 빈으로 등록한다.
이렇게 Advisor들을 빈으로 등록하면, AutoCreator는 Advisor들을 조회하여 스프링으로 등록될 대상 객체들의 프록시 객체가 생성되어야 하는지 여부를 판단한다.
프록시 객체가 생성되어야 한다고 판단될 경우 프록시 객체를 생성하여 스프링 빈으로 등록한다.
그렇지 않다면 실제 객체를 스프링 빈으로 등록한다.
// implementation 'org.springframework.boot:spring-boot-starter-aop'라는 라이브러리를 등록했다.
// 때문에 AnnotationAwareAspectJAutoProxyCreator라는 빈후처리기가 이미 스프링 빈으로 등록되어 있다.
// 이 빈후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
// 어드바이저의 포인트컷은 총 2가지에 사용된다.
// 1. 프록시 적용 여부 판단: 클래스 + 메소드 조건을 통해 프록시를 생성할 필요가 있는지를 판단한다.
// 2. 어드바이스 적용 여부 판단: 프록시가 호출되었을 때 프록시 로직을 적용해야하는 지 여부를 판단한다.
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
...
// hello.proxy.app 하위만 프록시, 어드바이스 적용 대상이다. 단 noLog 메소드는 제외이다.
@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);
}
}