스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈후처리기를 사용하면 됩니다. 이 기능은 막강해서 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능합니다.
빈 등록 과정 을 빈 후처리기와 함께 살펴봅시다.
1. 생성
= 스프링 빈 대상이 되는 객체를 생성합니다.
2. 전달
= 생성된 객체를 빈 저장소에 등록하기 직전에 '빈 후처리기'에 전달합니다.
3. 후 처리 작업
= 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 달느 객체로 바꿔치기 할 수 있습니다.
4. 등록
= 빈 후처리기는 빈을 반환합니다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록됩니다.
: 우선 일반적인 스프링 빈 등록 과정을 살펴보겠습니다.
public class BasicTest {
@Test
void basicConfig(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
// A는 빈으로 등록된다.
// .getBean(String name, Class<T> requiredType)
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");
}
}
}
new AnnotationConfigApplicationContext(BasicConfig.class)
는 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주는 코드입니다. 여기서 BasicConfig.class 설정 파일은 스프링 빈으로 등록됩니다.
AnnotationConfigApplicationContext는 자바 설정에서 정보를 읽어와 빈 객체를 생성 및 관리합니다. 스프링 컨테이너를 생성하고 설정 정보를 참고해서 스프링 빈도 등록하고 의존관계도 설정하는 것입니다.
= 빈 후처리기를 통해서 A객체를 B객체로 바꿔치기 하는 코드를 작성해볼 것입니다.
: 빈 후처리기를 사용하려면 BeanPostProcessor
인터페이스를 구현하고, 스프링 빈으로 등록하면 됩니다. 해당 인터페이스에는 두 개의 메소드가 존재하는데
postProcessBeforeInitialization
: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서입니다.postProcessAfterInitialization
: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서입니다.BeanPostProcessorTest
public class BeanPostProcessorTest {
...
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor{
// A를 B로 바꾸는 post Processor
// 디폴트가 있으면 오버라이딩을 필수로 안해도 된다.
// 근데 기능이 필요하니 오버라이딩 함
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("bean name = {}, bean = {}", beanName, bean);
if(bean instanceof A){
return new B();
}
return bean;
}
}
}
: 인터페이스인 BeanPostProcessor
를 구현하고 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작합니다. 이 빈후처리기는 A객체를 새로운 B객체로 바꿔치기합니다.
정리
: 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트입니다. 이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강합니다. 여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻합니다. 일반적으로 스프링 컨테이너가 등록하는, 특히 컴퓨넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있게 됩니다. 즉, 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻입니다.
: 빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보는 과정입니다. 이렇게 하면 수동으로 등록하는 빈은 물론이고 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있습니다.
PackageLogTraceProxyPostProcessor
: 원본 객체를 프록시 객체로 변환하는 역할을 하는 클래스입니다.
: 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor
가 필요하기 때문에 이 부분은 외부에서 주입 받도록 합니다.
: 모든 스프링 빈들에 프록시를 적용할 필요는 없습니다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용합니다. 여기서는 hello.proxy.app
과 관련된 부분에만 적용하면 됩니다. 다른 패키지의 객체들은 원본 객체를 그대로 반환합니다.
: 프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환합니다. 따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록됩니다. 원본 객체는 스프링 빈으로 등록되지 않습니다.
@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 {
// 프록시 적용 대상 여부 체크
// 프록시 적용 대상이 아니면 원본을 그대로 반환
String packageName = bean.getClass().getPackageName();
if(!packageName.startsWith(basePackage)){
return bean;}
// 프록시 대상이면 프록시를 만들어서 반환
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
return proxy;
}
}
BeanPostProcessorConfig
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
//V3는 component scan의 대상이 되니까 import 필요 없음
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);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
PointCut = 특정 조건에 의해 필터링 된 JoinPoint. 수많은 JoinPoint 중에서 특정 메서드에서만 공통 기능을 수행 시키기 위해서 사용합니다.
출처 : https://wpunch2000.tistory.com/22
프록시 적용 대상 여부 체크
: 직접 등록한 스프링 빈 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들도 "빈 후처리기"로 넘어옵니다. 그래서 어떤 빈을 프록시로 만들 것인지에 대한 기준이 필요합니다! 위의 예제에서는 basePackage
를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만들었습니다.
: 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다. 따라서 모든 객체를 프록시로 만들 경우 오류가 발생합니다.
빈 후처리기 - 정리
: 빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있게 됐습니다. 그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있습니다.
: 그래서 수많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 됩니다.
프록시의 적용 대상 여부를 여기서는 "패키지"를 기준으로 설정했습니다. 그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같습니다. 포인트컷은 이미 '클래스', '메서드 단위'의 필터 기능을 가지고 있기 때문에 프록시 적용 대상 여부를 정밀하게 설정할 수 있습니다.
참고로 어드바이저는 포인트 컷을 가지고 있기 때문에 포인트 컷을 확인할 수 있습니다.
결과적으로 포인트 컷은 다음 두 곳에서 사용 됩니다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용합니다. (빈 후처리기 -> 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할지 판단합니다. (프록시 내부 확인)
자동 프록시 생성기 AutoProxyCreator
: build.gradle 수정을 통해 AnnotationAwareAspectJAutoProxyCreator
라는 빈 후처리기가 스프링 빈에 자동으로 등록됩니다.
: 자동으로 프록시를 생성해주는 빈 후처리기입니다.
: 이는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해줍니다.
: Advisor 안에는 포인트컷 + advice가 이미 모두 포함되어있으므로, Advisor만 알고 있으면 포인트컷으로 어떤 스프링 빈에 프록시를 적용해야할지 알 수 있습니다. 그 후 Advice로 부가 기능을 적용하면 됩니다.
자동 프록시 생성기의 작동과정을 알아봅시다.
1. 스프링 빈 대상이 되는 객체를 생성합니다.
2. 생성된 객체를 빈 후처리기에 전달합니다.
3. 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회합니다.
4. 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단합니다. 이때 객체의 클래스 정보는 물론이고 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해봅니다. 그래서 조건이 하나라도 만족하면 프록시 적용 대상이 됩니다.
5. 프록시 적용 대상이면 프록시를 생성하고 반환합니다. 프록시 적용 대상이 아니라면 원본 객체를 반환합니다.
6. 반환된 객체는 스프링 빈으로 등록됩니다.
=> 여기서 프록시는 내부에 Advisor와 실제 호출해야할 대상 객체(target)을 알고 있습니다.
AutoProxyConfig
@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*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
-> advisor만 등록하면 됩니다. 빈 후처리기는 스프링의 자동 프록시 생성기를 사용합니다.
포인트컷은 두 가지에 사용됩니다.
1. (생성단계) 프록시 적용 여부 판단
: 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크합니다
: 클래스+메서드 조건을 모두 비교합니다. 조건에 맞는 것이 하나라도 있다면 프록시를 생성합니다.
2. (사용단계) 어드바이스 적용 여부 판단
: 프록시가 호출되었을 때 부가 기능인 advice를 적용할지 말지 포인트컷을 보고 판단합니다.
=> 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라 포인트컷으로 한 번 필터링해서 advice가 사용될 가능성이 있는 곳에만 프록시를 생성합니다.
: 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷에 대해 설명할 것입니다.
AspectJExpressionPointcut
: AspectJ라는AOP에 특화된 포인트컷 표현식을 적용할 수 있다. 현재 단계에서는 특별한 표현식으로 복잡한 포인트컷을 만들 수 있는 것이라 이해하면 됩니다.
AutoProxyConfig : advisor2 추가
@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);
}
여기서 는 모든 반환 타입을 의미하고 hello.proxy.app..은 해당 패키지와 그 하위 패키지를 의미하고 (..)는 *는 모든 메서드 이름,(..)파라미터는 상관없음을 의미합니다.
=> 그러니까 hello.proxy.app 패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 됩니다.
: 그런데 위의 코드는 패키지를 기준으로 포인트컷을 매칭했으므로 no-log에서도 로그가 출력됩니다.
@Bean
public Advisor advisor3(LogTrace logTrace){
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
hello.proxy.app 패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되, noLog()메서드는 제외하라는 뜻으로 변경해주면 됩니다.
예를 들어 어떤 스프링 빈이 advisor1, advisor2가 제공하는 포인트컷의 조건을 모두 만족하면 "프록시 자동 생성기는 프록시를 몇 개 생성할까요?"
: 하나만 생성합니다. 왜냐하면 Proxy Factory가 생성하는 프록시는 내부에 여러 Advisor들을 포함할 수 있기 때문입니다.
프록시 자동생성기 상황별 정리
== 즉 이러한 자동프록시 생성기 덕분에, 개발자는 Advisor만 스프링 빈으로 등록하면 됩니다.