이전 글에서는 프록시 팩토리의 장점과 한계점에 대해 알아보았습니다.
한계점을 다시 정리해보자면 다음과 같습니다.
- 프록시를 적용할 스프링 빈의 갯수만큼 프록시를 생성하여 빈으로 등록해주어야 한다. (설정 파일이 관리가 힘들다.)
- 컴포넌트 스캔으로 등록된 스프링 빈에는 적용할 수 없다.
이러한 문제들을 해결하기 위해 실무에서는 빈 후처리기(BeanPostProcessor)를 사용합니다.
일반적으로 @Bean 이나 @Component를 사용하여 빈등록을 하면 스프링은 대상 객체를 생성하고, 스프링 컨테이너 내부의 빈 저장소에 등록합니다.
하지만 빈등록을 하기 전에 빈을 원하는대로 조작할 수 있는 기능을 제공해주는 것이 바로 빈 후처리기입니다.
빈 후처리기(BeanPostProcessor)
생성 방법
빈후처리기는 BeanPostProcessor 인터페이스를 구현하여 생성합니다.
BeanPostProcessor의 구조는 다음과 같습니다.
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
두 메서드 중 하나의 메서드만 오버라이딩하여 사용할 수 있습니다. 두 메서드의 차이점은 빈 후처리기의 후처리기 시점입니다.
- postProcessBeforeInitialization() : 객체 생성 후, 초기화 작업 이전에 후처리를 진행한다.
- postProcessAfterInitialization() : 객체 생성 후, 초기화 작업 이후에 후처리를 진행한다.
예시
package blog.proxy.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
public class PostProcessor {
@Test
void postProcessor(){
ApplicationContext ac = new AnnotationConfigApplicationContext(PostProcessorConfig.class);
B beanA = ac.getBean("beanA", B.class);
beanA.run();
}
@Configuration
static class PostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean(name = "beanB")
public B b(){
return new B();
}
@Bean
public AToBBeanPostProcessor beanPostProcessor(){
return new AToBBeanPostProcessor();
}
}
static class A{
void run() {
log.info("A.run() 실행");
}
}
static class B{
void run(){
log.info("B.run() 실행");
}
}
static class AToBBeanPostProcessor implements BeanPostProcessor{
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("[빈후처리기 실행] bean = {}, beanName = {}", bean, beanName);
if (bean instanceof A) { //bean 이 class A의 인스턴스라면
return new B(); //새로운 B 객체를 생성해서 반환한다.
}
return bean;
}
}
}
각 스프링빈 생성 과정은 다음과 같습니다.
결과
ApplicationContext.getBean() 을 이용해 빈이름(beanA)으로 빈을 가져와 run()메서드를 실행했지만, 출력된 로그는 "B.run() 실행" 이었습니다.
이처럼, 빈후처리기를 통해 등록할 빈을 변경 및 조작할 수 있습니다. 일반적으로 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 수 있는 방법이 존재하지 않지만, 이 방법을 이용하면 개발자가 등록하는 모든 빈을 조작할 수 있습니다.
빈후처리기 프록시에 적용하기
빈 후처리기를 사용하여 실제 객체 대신 프록시를 스프링 빈으로 등록해보겠습니다.
동작 방식은 다음과 같습니다.
예시
Target.java
@Slf4j
public class Target {
public void run(){
log.info("Target.run() 실행");
}
}
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@Slf4j
public class MyAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("[MyAdvice] 부가기능 실행");
Object result = invocation.proceed();
log.info("[MyAdvice] 부가기능 실행");
return result;
}
}
ProxyBeanPostProcessor.java
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
public class ProxyBeanPostProcessor implements BeanPostProcessor {
private Advisor advisor;
public ProxyBeanPostProcessor(Advisor advisor) {
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
String packageName = bean.getClass().getPackageName(); //실제 대상 오브젝트 클래스의 패키지 경로
if (!packageName.startsWith("blog.proxy.postprocessor")) { //해당 경로의 클래스 오브젝트에만 빈 후처리기 적용
return bean;
}
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
return proxy;
}
}
PostProcessorConfig.java
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PostProcessorConfig {
@Bean
public Target target(){
return new Target();
}
@Bean
public DefaultPointcutAdvisor advisor(){
MyAdvice myAdvice = new MyAdvice();
return new DefaultPointcutAdvisor(Pointcut.TRUE, myAdvice);
}
@Bean
public ProxyBeanPostProcessor proxyBeanPostProcessor(){
return new ProxyBeanPostProcessor(advisor());
}
}
- Target.java : 프록시 객체의 실제 대상이 되는 클래스로 간단한 로그를 출력하는 run() 메서드를 가지고 있습니다.
- MyAdvice.java : 실제 대상 오브젝트 요청 위임 전후에 로그를 출력해주는 어드바이스입니다.
- ProxyBeanPostProcessor.java : 내부에 프록시 팩토리를 사용하여 입력으로 받은 빈 객체를 프록시로 변경하여 반환합니다. 빈 후처리기를 빈으로 등록하면, 빈으로 등록되는 모든 오브젝트에 대해 후처리 작업을 진행합니다. 만약 메서드에 final 키워드가 붙어있다면 스프링은 오류가 발생합니다. 따라서, "blog.proxy.postprocessor" 의 하위 경로에 있는 클래스 오브젝트에만 적용되도록 패키지 경로를 검사하는 코드를 추가하였습니다.
- PostProcessorConfig.java : 위 3개의 클래스를 빈으로 등록합니다.
테스트 코드
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@Slf4j
@SpringBootTest
public class PostProcessorTest {
@Autowired
Target target;
@Test
void postProcessor(){
target.run();
log.info("target.getClass = {}", target.getClass());
log.info("target.isAopProxy = {}", AopUtils.isAopProxy(target));
}
}
결과
'[Back-end] > [Spring]' 카테고리의 다른 글
[Spring] @Aspect (2) | 2021.12.10 |
---|---|
[Spring] 스프링의 빈후처리기(AnnotationAwareAspectJAutoProxyCreator) (0) | 2021.12.07 |
[Spring] 다이내믹 프록시(DynamicProxy) (1) | 2021.11.30 |
[Spring] 프록시와 디자인패턴 (1) | 2021.11.30 |
[Spring] DispatcherServlet이란? (0) | 2021.09.18 |