빈 후처리기(BeanPostProcessor)
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶을때 이용한다. BeanPostProcessor 를 번역하면 빈 후처리기로 이름 그대로 빈 생성 후에 어떤 처리를 하는 용도로 사용한다.
객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.
여기서 조작이라는 것은 해당 객체의 특정 메소드를 호출하는 것을 뜻한다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 즉 빈(Bean) 객체를 프록시로 교체하는 것도 가능하다는 의미이다.
프록시 팩토리 (ProxyFactory) 를 통해 프록시를 편리하게 생성할 수 있었고, 스프링의 어드바이저 (Advisor) , 어드바이스 (Advice), 포인트컷 (PointCut) 이라는 개념으로 부가기능의 적용여부를 편리하게 정의할 수 있었다. 그러나 프록시의 많은 설정이 요구되는 문제와 컴포넌트 스캔의 경우 프록시가 생성되지 않는 문제가 있었다.
- 많은 설정 요구
프록시를 직접 스프링 빈으로 등록하는 @Configuration 설정 파일은 프록시와 관련하여 설정이 너무 많다는 문제가 발생한다. 클래스 수 만큼 설정 코드가 요구되므로 설정하는데 많은 비용이 소모된다. - 컴포넌트 스캔의 프록시 적용 문제
컴포넌트 스캔을 사용하는 경우, 프록시를 직접 스프링빈으로 등록하는 설정코드로는 프록시 적용이 불가능했다. 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 했기 때문이다. @Configuration 으로 프록시를 적용하려면, 실제 객체 대신 프록시를 스프링 컨테이너에 빈으로 등록해야 한다. 컴포넌트 스캔은 실제 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.
빈 등록 과정
- 생성 : 스프링 빈 대상 객체를 생성 (@Bean, 컴포넌트 스캔 모두 포함)
- 전달 : 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달
- 후 처리 : 빈 후처리기는 객체를 조작하거나, 다른 객체로 바꿔치기 할 수 있다.
- 등록 : 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기하면 다른 객체가 빈 저장소에 등록된다.
예제코드
1. 클래스 : Phone, Radio
2. 빈 후처리기 (BeanPostProcessor) 통해서 Phone 객체를 Radio 객체로 바꿔서 스프링 빈에 등록 해보기
@Slf4j
public class Phone {
public void phoneCall() {
log.info("phone call");
}
}
@Slf4j
public class Radio {
public void radioCall() {
log.info("radio call");
}
}
/** 스프링빈 설정 */
@Slf4j
@Configuration
public class BeanPostProcessorConfig {
// "beanPhone" 라는 이름으로 Phone 빈을 등록한다.
@Bean(name = "beanPhone")
public Phone phone() {
return new Phone();
}
/** 빈 후처리기 */
@Bean
public PhoneToRadioPostProcessor toRadioProcessor() {
return new PhoneToRadioPostProcessor();
}
}
/**
* 스프링이 제공하는 빈 후처리기 BeanPostProcessor
*/
@Slf4j
public class PhoneToRadioPostProcessor implements BeanPostProcessor {
// 객체 생성 , 초기화가 발생하고 후(post) 처리(process)
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("print beanName, bean \n => beanName = {} \n => bean = {}", beanName, bean);
// Phone 객체이면 Radio 객체로 바꿔서 리턴
if (bean instanceof Phone) {
return new Radio();
}
return bean;
}
}
실행 (테스트 코드)
/** 스프링이 제공하는 BeanPostProcessor 인터페이스 사용하여 객체 바꿔치기 */
public class BeanPostProcessorTest {
@Test
void postProcessor() {
ApplicationContext applicationContext =
new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
// beanPhone 이름으로 Radio 객체가 빈으로 등록된다.
Radio radio = applicationContext.getBean("beanPhone", Radio.class);
radio.radioCall();
// Phone 은 빈으로 등록되지 않으므로 NoSuchBeanDefinitionException 발생
Assertions.assertThrows(NoSuchBeanDefinitionException.class,
() -> applicationContext.getBean(Phone.class));
}
}
실행 결과
...
...PhoneToRadioPostProcessor - print beanName, bean
=> beanName = beanPhone
=> bean = hello.proxy.postprocessorbasic.code.Phone@6bc28a83
...Radio - radio call
결과를 보면 "beanPhone" 이라는 스프링 빈 이름에 Phone 객체 대신에 Radio 객체가 등록 된 것을 확인할 수 있다.
Phone 는 스프링빈으로 등록되지 않는다.
정리
예제 코드를 통해 BeanPostProcessor 을 이용하여 객체를 바꿔치기할 수 있다는 것을 확인했다.
즉, 빈 후처리기(BeanPostProcessor) 는 빈 객체를 조작하거나 다른 객체로 바꾸어 버릴 수 있다.
여기서 조작이라는 것은 해당 객체의 특정 메소드를 호출하는 것을 말한다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 즉 빈 객체를 프록시로 교체하는 것도 가능하다는 의미이다.
문제 해결
빈 후처리기 (BeanPostProcessor) 에서 프록시 적용 대상 여부를 확인하여 프록시 적용 대상이면 프록시를 만들어 리턴하고, 프록시 적용 대상이 아니면 원본 객체 그대로 리턴하면 된다. 이렇게 함으로써 @Configuration 을 통해 수동으로 등록한 빈 뿐만 아니라 컴포넌트 스캔을 사용하는 빈까지 모두 수 많은 프록시 생성 코드를 작성하지않고 편하게 프록시를 적용할 수 있다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해주기 때문이다.
즉, 어플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 변경하지 않아도 된다.
빈 후처리기 (BeanPostProcessor) 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다.
스프링이 제공하는 빈 후처리기
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어 제공한다.
프록시의 적용 대상 여부를 확인할때, 포인트컷 (Pointcut)을 사용하면 프록시 대상 여부를 정밀하게 설정할 수 있다. 클래스, 메소드 단위의 필터 기능을 가지고 있기 때문이다. 어드바이저는 1개의 어드바이스와 1개의 포인트컷으로 구성되므로 어드바이저를 통해 포인트컷을 확인할 수 있다. 참고로 스프링 AOP 는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
📍 포인트컷이 사용되는 두 곳
- 생성 : 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다.
(빈 후처리기 - 자동 프록시 생성) - 사용 : 프록시의 메소드가 호출 되었을 때 어드바이스를 적용할 지 판단한다.
(프록시 내부)
・ 프록시를 모든 곳에 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다. 그 때문에 자동 프록시 생성기는 포인트컷으로 필터링해서 사용되는 곳에만 프록시를 생성하는 것이다.
◾라이브러리 추가 spring-boot-starter-aop
이 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없을 때는 @EnableAspectJAutoProxy 를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. (AopAutoConfiguration)
∙Gradle 을 이용하는 경우 : build.gradle
dependencies {
// aspectj 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
∙Maven 을 이용하는 경우 : pom.xml
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.x.x</version> <!-- 버전 -->
</dependency>
자동 프록시 생성기 (AutoProxyCreator)
스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기 이다.
이 빈 후처리기는 스프링 빈으로 등록된 Advisor 들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
Advisor 는 1개의 Advice 와 1개의 Pointcut 으로 구성되어 있다. 따라서 Advisor 만 알고 있으면 그 안에 있는 포인트컷으로 어떤 스프링빈에 프록시를 적용할지 알 수 있고, 어드바이스로 부가기능을 적용하면 된다.
자동 프록시 생성기의 작동 과정
- 생성 : 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
- 전달 : 생성된 객체를 빈 저장소에 등록하기 전에 빈 후처리기에 전달한다.
- 모든 Advisor 빈 조회 : 자동 프록시 생성기 (빈 후처리기) 는 스프링 컨테이너에서 모든 Advisor 를 조회한다.
- 프록시 적용 대상 체크 : 조회한 Advisor 에 포함되어 있는 Pointcut 을 사용해서 해당 객체가 프록시 적용 대상인지 확인한다. 이때 객체의 클래스 정모 뿐만아니라 모든 메소드를 포인트컷에 모두 매칭해본다. 그리고 하나라도 조건에 만족하는 것이 있으면 프록시 적용 대상이 된다.
- 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하고 반환하여 프록시를 스프링 빈으로 등록한다. 프록시 적용 대상이 아니라면 원본 객체를 반환하여 원본 객체가 스프링 빈으로 등록된다.
- 빈 등록 : 반환된 객체는 스프링 빈으로 등록된다.
정리하면, 스프링 부트 라이브러리로 자동 프록시 생성기 AnnotationAwareAspectJAutoProxyCreator 덕분에 편하게 프록시를 적용할 수 있다. Advisor 만 스프링 빈으로 등록하면 된다.