*인프런 김영한 강사님의 강좌를 참고하여 정리한 내용입니다.*
ProxyFactory를 통해 Proxy를 생성방법을 통합하고 일관성을 유지할 수 있었다. 하지만 Proxy 기능을 적용하고자 하는 클래스들이 많을수록 설정 파일이 복잡해졌고, 빈 등록시에 컴포넌트 스캔을 사용하면 컴파일 과정에서 빈을 등록해버리기 때문에 Proxy를 생성할 수 없었다.
이러한 문제를 어떻게 해결할 수 있을까?
바로 빈 후처리기!!
일반적인 스프링의 빈 등록 과정이다. @Bean이나 컴포넌트 스캔으로 빈을 등록하면, 스프링은 대상 객체를 생성하여 스프링 컨테이너의 빈 저장소에 등록한다.
빈 후처리기를 사용하게 되면 스프링 빈 저장소에 저장하기 전, 빈 후처리기에서 객체를 Proxy로 변환하는 작업을 진행하게 된다!
@Bean, 컴포넌트 스캔 모두 포함)@Slf4j
public class BasicTest {
@Configuration
static class BasicConfig {
// beanA 라는 이름으로 A 객체를 스프링 빈으로 등록.
@Bean(name = "beanA")
public A a() {
return new A();
}
}
static class A {
public void helloA() {
log.info("call A");
}
}
@Test
void basicConfig() {
// BasicConfig를 빈으로 등록 (내부 메서드 포함)
ApplicationContext ac = new
AnnotationConfigApplicationContext(BasicConfig.class);
// 빈 가져오기 beanName, ReturnType
A a = ac.getBean("beanA", A.class);
}
}
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
빈 후처리기(BeanPostProcessor)를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록해야 한다!
postProcessBeforeInitialization는 객체 생성 이후, @PostConstruct 초기화가 발생하기 전에 호출되는 프로세서이다.
postProcessAfterInitialization는 객체 생성 이후, @PostConstruct 초기화가 발생한 다음에 호출되는 프로세서이다.
@Slf4j
public class BeanPostProcessorTest {
@Configuration
static class BeanPostProcessorConfig {
// beanA 라는 이름으로 A 객체를 스프링 빈으로 등록.
@Bean(name = "beanA")
public A a() {
return new A();
}
// 빈 후처리기 프로세서 빈으로 등록.
@Bean
public AToBPostProcessor testProcessor() {
return new AToBPostProcessor();
}
}
static class A {
public void helloA() {
log.info("call A");
}
}
static class B {
public void helloB() {
log.info("call B");
}
}
// 빈 후처리기 구현체
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
// 빈이 TypeOf A이면 B객체를 return 해라.
if (bean instanceof A) {
return new B();
}
// 아니면 원본 빈 return.
return bean;
}
}
@Test
void basicConfig() {
// BasicConfig를 빈으로 등록 (내부 메서드 포함)
ApplicationContext ac = new
AnnotationConfigApplicationContext(BasicConfig.class);
// 빈 가져오기 beanName, ReturnType
B b = ac.getBean("beanA", B.class);
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean(A.class));
}
}
빈 후처리기를 구현하고 빈으로 등록한 결과이다. BasicConfig.class를 빈으로 등록했는데 내부에는 A를 return하는 메서드, 빈 후처리기 메서드가 있다.
테스트 코드를 확인해보면 beanName은 beanA 지만 B객체를 가져오는 것을 확인할 수 있는데, 빈 후처리기(BeanPostProcessor)에 A타입의 빈은 B객체로 저장하라는 로직이 있기 때문이다. 또한, Assertion 검증 코드를 확인해보면 A객체는 스프링 빈으로 등록조차 되지 않는다는 것을 알 수 있다.
빈 후처리는 빈을 조작하고 변경할 수 있는 후킹 포인트이다. 모든 빈이 등록되기 전 중간 단계에서 조작할 수 있다. (빈 객체를 프록시로 교체도 가능)
@PostConstruct는 스프링 빈이 생성된 이후에 빈을 초기화 한다. 어떠한 방식으로 초기화 하는걸까?
스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하여 @PostConstruct 어노테이션이 붙은 메서드를 초기화 한다. 따라서 스프링 또한 내부의 기능을 확장하기 위해 빈 후처리기를 사용하는 것을 알 수 있다.
그렇다면 BeanPostProcessor를 실전에 어떻게 적용시켜야 할까?
public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisort advisor
public PackageLogTraceProxyPostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean) throws BeansException {
// 프록시 적용 대상 검증로직
// 빈 객체가 포함된 패키지명
String packageName = bean.getClass().getPackageName();
// 해당 빈의 패키지 명이 프록시를 생성하고자 하는 패키지명 basePackage에 포함되지 않다면
if (!packageName.startWith(basePackage) {
// 원본 빈 객체를 반환
return bean;
}
// 프록시 대상이라면 프록시 반환
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.adAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
return proxy;
}
}
PackageLogTraceProxyPostProcessor 빈 후처리기를 빈으로 등록하게 되면, 알아서 빈 후처리기 로직이 작동한다!
이전의 ProxyFactory "만" 사용한 경우에는 Config.class에 등록한 모든 빈 초기화 코드에 ProxyFactory를 통해 프록시 객체를 반환하는 로직이 포함되었으며 빈 후처리기를 통해 Config.class에 모든 프록시 생성 로직이 중복되는 문제가 해결되었다.
또한 빈 후처리기를 통해 빈을 등록하기 전 프록시 객체로 변환하기 때문에 컴포넌트 스캔을 통해 프록시를 적용할 수 없는 문제또한 해결되었다!
프록시를 자동으로 생성할 수 있는 방법이 있는데, 스프링의 빈 후처리기를 사용하면 Proxy를 직접 생성할 필요 없이 자동으로 생성한다.. (이미 대부분의 기능은 스프링 부트에 모두 구현되어있다)
implementation 'org.springframework.boot:spring-boot-starter-aop'
스프링의 빈 후처리기를 사용하기 위해서는 aspectJ 라는 라이브러리가 필요하다. 따라서 aspectJ 관련 라이브러리를 등록하고 AOP 관련 클래스를 빈으로 등록하도록 해당 설정 정보를 gradle 추가하자.
앞선 스프링 부트 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동 등록된다. 이름 그대로 "AspectJ 어노테이션을 인지하여 자동으로 프록시를 생성한다".
해당 빈 후처리기는 Advisor들을 찾아 프록시가 필요한 곳에 자동으로 프록시를 생성하여 적용한다.
Advisor는 Pointcut과 Adivce가 포함되어 있다. 그래서 Pointcut으로 어떤 스프링 빈에 프록시를 적용할지 알 수 있는것이다!
보다시피 스프링 컨테이너 내의 저장소에서 빈으로 등록된 모든 Advisor를 조회한다.
앞서 조회한 Adivosr의 Pointcut을 참고하여 해당 객체가 프록시의 적용 대상인지 검증하며, 만약 하나의 조건(10개의 메서드 중 하나라도)이라도 만족한다면 프록시 적용 대상이 된다.
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
다음과 같은 Advisor를 빈으로 등록하기만 하면 되며 빈 후처리기 인터페이스를 구현하여 적용하는 과정은 생략해도 된다. AnnotationAwareAspectJAutoProxyCreator가 빈 후처리기를 알아서 등록해주기 때문이다.
여기서 중요한 포인트가 하나 있다!
만약 orderController 클래스의 request(), noLog() 메서드가 있다고 하자. noLog() 메서드는 프록시가 필요없는 메서드이다. 하지만 request() 메서드는 프록시의 부가 기능 로직이 필요하기 때문에 orderController 클래스는 프록시로 생성해야 한다.
자동 프록시 생성기가 사용하는 경우이며, 조회한 모든 Advisor의 Pointcut을 통해 해당 빈이 프록시를 생성할 필요가 있는지 체크한다.
이미 프록시가 생성되어 호출된 경우이며, 부가 기능인 Advice를 적용할지 Pointcut을 보고 판단한다. request()는 프록시를 적용, noLog()는 프록시를 적용하지 않는다.
이 빈 후처리기는 AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.
@Bean
public Advisor advisor2(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);
}
다음과 같은 Advisor를 빈으로 등록하기만 하면 되며 빈 후처리기 인터페이스를 구현하여 적용하는 과정은 생략해도 된다. AnnotationAwareAspectJAutoProxyCreator가 빈 후처리기를 알아서 등록해주기 때문이다.
advisor1과 다른점은 AspectJExpressionPointcut를 사용함으로서 포인트 컷을 정규식으로 표현할 수 있다는 것이다.
execution:*:hello.proxy.app..:*(..):* 모든 메서드 이름, (..) 파라미터는 상관 없다hello.proxy.app 패키지와 하위 패키지의 모든 클래스에서 실행되는 모든 메서드에 Aspect(포인트컷 매칭)를 적용한다.
!:execution:*:hello.proxy.app..:noLog(..):(..) 파라미터는 상관 없다hello.proxy.app 패키지와 하위 패키지의 모든 클래스에서 실행되는 noLog() 메서드는 Aspect(포인트컷 매칭)를 적용하지 않는다.
스프링의 빈 후처리기인 AnnotationAwareAspectJAutoProxyCreator와 AspectJ의 정규식을 통해 쉽게 Pointcut의 필터링을 적용하고 Advisor를 빈으로 등록해 자동으로 프록시를 생성할 수 있게 되었다.
스프링 AOP의 @Aspect 어노테이션을 사용하면 더 편리하게 포인트 컷과 어드바이스를 만들어 프록시를 적용할 수 있다고 한다!