@Bean
이나 컴포넌트 스캔으로 스프링빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링빈을 조회해서 사용하면 된다.
빈 후처리기 - BeanPostProcessor
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면(실제 객체 대신에 프록시 객체를 스프링빈으로 등록하고 싶다면??) 빈 후처리기를 사용하면 된다. 빈 포스트 프로세서(BeanPostProcessor)는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.
빈 후처리기 기능
빈 후처리기의 기능은 매우 막강하다.
객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.
빈 등록 과정을 빈 후처리기와 함께 살펴보면 아래와 같다.
public class BasicTest {
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
//A는 빈으로 등록된다.
A a = applicationContext.getBean("beanA", A.class);
a.helloA();
//B는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.class));
}
@Slf4j
@Configuration
static class BasicConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
}
아래의 코드는 빈 후처리기를 적용한 코드이다.
즉, A객체를 B객체로 바꿔치기 해서 등록한다.
아래의 코드는 BeanPostProcessor 인터페이스이다.
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws
BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws
BeansException
}
@PostConstructor
같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.@PostConstruc
같은 초기화가 발생한 다음에 호출 되는 포스트 프로세서이다.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(A.class));
}
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@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);
if (bean instanceof A) {
return new B();
}
return bean;
}
}
}
AToBPostProcessor
BeanPostProcessor
를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.정리
빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.
여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는것을 뜻한다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.
참고 - @PostConstruct의 비밀
@PostConstruct
는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화라는 것이 단순히@PostConstructor
애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다.(함수를 미리 호출한다.)
따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 될것같다.
스프링은CommonAnnotationBeanPostProcessor
라는 빈 후처리기를 자동으로 등록하는데, 여기에서@PostConstructor
애노테이션이 붙은 메서드를 호출한다. 따라서 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.
빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자.
이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
더 나아가서 설정 파일에 있는 수많은 프록시 생성 코드도 한번에 제거할 수 있다.(빈 생성 과정에서 빈후처리기가 알아서 프록시를 대신 스프링빈에 등록해주니까 Config 파일에서 의존성을 복잡하게 걸어줄 필요가 없다.)
@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;
}
}
PackageLogTraceProxyPostProcessor
는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 Advisor
가 필요하기 때문에 이 부분은 외부에서 주입받도록 한다.hello.proxy.app
과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.@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);
}
}
@Bean logTraceProxyPostProcessor()
:특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링빈으로 등록한다. 빈 후처리기는 스프링빈으로만 등록하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보와 어드바이저를 넘겨준다.결과적으로 컴포넌트 스캔을 통해 등록된 빈들도 프록시를 적용할 수 있게 되었다.
basePackage
를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다.이전에 보았던 문제들이 빈 후처리기를 통해서 해결됐다.
문제1 - 너무 많은 설정
프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1. ProxyFactoryCOnfigV2
와 같은 설정파일은 프록시 관련 설정이 지나치게 많다는게 문제였다.
예를 들어 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통한 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야한다. 무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될것이다.
문제2 - 컴포넌트 스캔
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지의 학습 방법으로는 프록시 적용이 불가능했다.
왜냐하면 컴포넌트 스캔으로 이미 스프링컨테이너에 실제 객체를 스프링빈으로 등록을 다 해버린 상태이기 떄문이다.
좀 더 풀어서 설명하자면, 지금까지 학습한 방식으로 프록시를 적용하려면 원본 객체를 스프링 컨테이너에 빈으로 등록하는것이아니라 ProxyFactoryConfigV1
에서 한것 처럼 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야한다. 그런데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능했다.
문제 해결
빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다. 그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링빈으로 등록할 수 있다.
덕분에 애플리케이션의 수많은 스프링빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트스캔을 사용해도 프록시가 모두 적용된다.
중요
프록시의 적용 대상 여부를 여기서는 간단히 패키지를 기준으로 설정했다. 그런데 잘 생각해보녀 포인트컷을 사용하면 더 깔끔하다.
포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다.
결과적으로 포인트컷은 다음 두곳에서 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한곳에만 프록시를 적용한다(빈 후처리기- 자동프록시 생성)
2. 프록시의 어떤 메서드가 호출되었을떄 어드바이스를 적용할지 판단한다.(프록시 내부)
스프링이 제공하는 빈후처리기를 사용하기 위해서는 아래의 의존성을 추가하자.
implementation 'org.springframework.boot:spring-boot-starter-aop'
이 라이브러리를 추가하면 aspectjwever
라는 aspectJ
관련 라이브러리를 등록하고 스프링 부트가 AOP 관련 클래스를 자동으로 스프링빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy
를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다.
자동 프록시 생성기 - AutoProxyCreator
AnnotationAwareAspectJAutoProxyCreator
라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.Advisor
들을 자동으로 찾아서 프록시가 필요한곳에 자동으로 프록시를 적용해준다.(물론 Advisor
는 스프링빈으로 등록되어 있어야한다.)Advisor
안에는 PointCut
과 Advice
가 이미 모두 포함되어 있다. 따라서 Advisor
만 알고있으면 그안에 있는 PointCut
으로 어떤 스프링 빈에 프록시를 적용해야할지 알 수 있다. 그리고 Advice
로 부가 기능을 적용하면 된다.참고
스프링 컨테이너가 제공하는빈 후처리기는 프록시 적용 대상 여부를 체크해서 꼭 필요한곳에만 프록시를 적용함(포인트컷으로) 내가 만든 빈후처리기가 아니라 스프링에서 제공하는것!!!
자동 프록시 생성기의 작동 과정은 아래와 같다.
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);
}
}
중요 : 포인트컷은 2가지에 사용된다.
- 프록시 적용 여부 판단 - 생성 단계
- 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
- 클래스 + 메서드 조건을 모두 비교한다. 이때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는것이 하나라도 있으면 프록시를 생성한다.
- 만약 조건에 맞는것이 하나도 없으면 프록시를 생성할 필요가 없으므로 프록시를 생성하지 않는다.
- 어드바이서 적용 여부 판단 - 사용 단계
- 프록시가 호출되었을때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.
- 앞서 설명한 예에서 `orderControllerV1'은 이미 프록시가 걸려있다.
orderControllerV1
의request()
는 현재 포인트컷조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고, target을 호출한다.orderControllerV1
의noLog()
는 현재 포인트컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 바로target
만 호출한다.
참고 : 프록시를 모든곳에 생성하는것은 비용낭비이다. 꼭 필요한곳에 최소한의 프록시를 적용해야한다. 그래서 자동 프록시 생성기는 모든 스프링빈에 프록시를 적용하는것이 아니라 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는곳에만 프록시를 생성한다.
AspectJExpressionPointcut
AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다. 간단하게 좀 더 복잡한 포인트컷을 만들 수 있다고 이해하자.
@Bean
public Advisor advisor2(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
쉽게 이야기해서 hello.proxy.app 패키지와 그하위 패키지의 모든 메서드는 포인트컷의 매칭대상이 된다.
참고
spring-boot-start-aop 라이브러리를 넣으면 자동프록시 생성기인 AutoProxyCreator가 스프링빈에 등록됨, 자동프록시 생성기가 PostProcessor임
자동프록시 생성기 과정
어드바이저가 스프링빈으로 등록되어있어야함.
예를 들어서 어떤 스프링 빈이 advisor1 , advisor2 가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까? 프록시 자동 생성기는 프록시를 하나만 생성한다. 왜냐하면 프록시 팩토리가 생성하는 프록시는 내부에 여러 advisor 들을 포함할 수 있기 때문이다. 따라서 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다
프록시 자동 생성기 상황별 정리
해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 스프링 빈후처리기