빈 후처리기를 사용하면 기존에 컴포넌트 스캔을 활용할 수 없는 문제와 각 빈마다 프록시 팩토리를 만들어야 하는 중복 문제를 어떻게 개선하는가?
빈 후처리기 사용 시 대상 여부 판단에서 주의할 점은?
빈 후처리기 사용 시 포인트컷이 사용되는 2가지 부분은?
하나의 빈에 여러 어드바이저가 적용된다면 어떠한 현상이 벌어지는가?
빈 후처리기
일반적으로 @Bean이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록
이후에서는 스프링 컨테이너를 통해 빈 저장소에 등록된 빈을 조회해서 사용
빈 후처리기(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;
}
}
BeanPostProcessor를 구현하는 빈후처리기 클래스 생성
postProcessAfterInitialization 함수를 오버라이드
만약에 bean이 A면 B를 반환하는 코드로 빈 바꿔치기 시도
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
beanA도 스프링 빈으로 등록하고 앞서 만든 빈후처리기도 스프링 빈으로 등록
@Test
void postProcessor() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
// beanA 이름으로 B 객체가 빈으로 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
// A는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}
스프링 컨테이너에서 beanA라는 빈 가져오기 시도
하지만, beanA를 가져왔지만 실제로는 beanB인 상황
beanA -> beanB로 바꿔치기 됨
이 말은 우리가 원하는대로 특정 bean을 프록시로 교체 가능하다는 말
빈 후처리기를 LogTrace에 적용
@Slf4j
public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
public PackageLogTraceProxyPostProcessor(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;
}
}
프록시 팩토리로 프록시를 만들어서 우리가 원하는 빈을 프록시로 반환해주려고 함
프록시 팩토리에 넣어줄 어드바이저는 외부에서 주입해줌
중요한 점은 지정한 basePackage가 아닌 경우에는 일반 bean을 리턴하게 한 부분
왜냐하면, 스프링은 우리가 직접 등록하는 빈 외에 기본 빈들이 무수히 많아서, 이러한 빈들에는 프록시 설정을 해 줄 필요가 없기 때문
또한, 특정 기본 빈들은 프록시 객체를 만들 수 없는 빈도 있어 오류가 생길 수 있음
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
@Bean
public PackageLogTraceProxyPostProcessor logTraceProxyPostProcessor(LogTrace logTrace) {
return new PackageLogTraceProxyPostProcessor("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);
// advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
빈 후처리기를 빈으로 등록
빈 후처리기의 파라미터로 베이스 페키지 경로를 넣어 줬음
어드바이저는 외부에서 주입해주기로 했으므로 어드바이저도 파라미터로 넣어 줌
어드바이저에는 포인트 컷과 어드바이스르 넣어 줘야 함
이 Configuration에는 더 이상 프록시 생성 코드가 존재하지 않음
빈 후처리기로 프록시 팩토리의 문제점 해결
프록시 생성을 하나의 부분으로 집중 가능
여러 스프링 빈마다 중복으로 프록시 팩토리 코드 넣어야 하는 중복 문제 해결
컴포넌트 스캔처럼 자동 생성 빈에도 프록시 적용 가능
스프링이 제공하는 빈 후처리기
스프링 부트는 자동으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기를 빈으로 등록
자동으로 프록시를 생성해주는 빈 후처리기
스프링 빈으로 등록된 Advisor를 자동으로 찾아서 프록시가 필요한 곳에 적용해줌
Advisor 안에는 Pointcut과 Advice가 포함되어 있기 때문에 Advisor만 알면 그 안의 Pointcut으로 어떤 스프링 빈에 프록시를 적용할 지 알 수 있음
Pointcut만 알면 어떤 스프링 빈에 프록시를 적용할 지 알 수 있다는 점이 핵심
객체의 모든 메서드를 포인트컷에 매칭해보고 하나라도 만족하면 프록시 대상, 하나도 만족하지 못하면 원본 객체를 스프링 빈으로 등록
스프링이 제공하는 빈 후처리기 사용 방법
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
// advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
위와 같이 어드바이저만 빈으로 등록하면 스프링이 프록시를 자동으로 생성해줌
더 이상 빈 후처리기를 우리가 만들어 줄 필요가 없음
스프링이 그냥 "request*", "order*", "save*"에 매칭되는 메서드를 가친 모든 빈을 advice를 적용한 프록시 빈으로 등록함.
포인트 컷이 사용되는 2곳
기존에 포인트 컷은 advice를 적용할 위치를 지정하는 하나의 용도로 사용되었지만 이젠 두 곳에서 사용 됨
프록시 적용 여부 판단 - 생성 단계
만약 포인트컷의조건에 맞는 메서드가 하나라도 있으면 프록시를 생성
만약 포인트컷의 조건에 맞는 메서드가 하나도 없으면 프록시를 생성할 필요가 없으므로, 프록시를 생성하지 않음
어드바이스 적용 여부 판단 - 사용 단계
프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지를 포인트컷을 보고 판단
포인트 컷 개선
@Bean
public Advisor advisor2(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
// advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
"request*", "order*", "save*" 와 같은 포인트 컷은 우리가 원치 않는 여러 메서드도 포함되는 문제가 발생할 수 있음