안녕하세요, 오늘은 스프링 프레임워크의 빈 생명주기 콜백과 컨테이너 확장에 대해서 알아보겠습니다.
스프링 프레임워크의 대표적인 기술로 IoC(Inversion of Control) 컨테이너가 있습니다.
IoC 컨테이너는 스프링 빈을 생성하고 관리를 하게 되는데,
콜백과 컨테이너 확장(Container Extension Points)으로 스프링 빈을 커스터마이징할 수 있습니다.
스프링 빈의 이벤트 Lifecycle을 간략하게 살펴보면 다음과 같습니다.
여기서 사용(4) 단계 전후로, 콜백함수를 지정하여 코드를 실행할 수 있습니다.
스프링에서 콜백함수 지정하는 방식으로 3가지를 제공하고 있습니다.
스프링에서는 InitializingBean
, DisposableBean
인터페이스를 제공하고 있습니다.
이름에서 유추할 수 있듯이, InitializingBean
은 스프링 빈 생성 이후에 실행할 함수를 제공하는 인터페이스이며, DisposableBean
인터페이스는 소멸 직전에 실행할 함수를 제공하는 인터페이스입니다.
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
public interface DisposableBean {
void destroy() throws Exception;
}
콜백을 지정할 빈에 해당 인터페이스를 구현하면, 각 단계에 맞게 BeanFactory가 콜백 함수를 실행시킵니다.
예제 코드는 Kotlin으로 작성되었습니다.
@Component
class SampleBean: InitializingBean, DisposableBean {
private val logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("SampleBean created")
}
override fun afterPropertiesSet() {
logger.info("SampleBean afterPropertiesSet")
}
override fun destroy() {
logger.info("SampleBean destroy")
}
}
해당 방식은 코드가 스프링 프레임워크와 결합이 강하게 이루어지므로 좋은 방식은 아닙니다.
스프링 빈 등록시에 @Bean
의 속성 initMethod
와 destroyMethod
를 통해 직접 지정해줄 수 있습니다.
예제 코드는 다음과 같습니다
/* SimpleBean.kt */
class SampleBean {
private val logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("SampleBean created")
}
fun init() {
logger.info("SampleBean init")
}
fun destroy() {
logger.info("SampleBean destroy")
}
fun close() {
logger.info("SampleBean close")
}
fun shutdown() {
logger.info("SampleBean shutdown")
}
}
/* Config.kt */
@Configuration
class Config {
@Bean(initMethod = "init", destroyMethod = "destroy")
fun sampleBean() = SampleBean()
}
@Bean
어노테이션을 사용할 때, destroyMethod
는 기본적으로 INFER_METHOD
로 지정되어 있습니다.
이는 개발자에게 편의를 제공하기 위한 것으로, 관습적으로 리소스 정리할 때 쓰이는 메소드 이름을 자동으로 찾아서 호출해줍니다.
메소드가 추론되기 위해서는,
1. 접근제어자가 public
이며,
2. 인자가 없어야 하고(no-args),
3. 이름이 "close"나 "shutdown"이어야 합니다.
이 추론 방식을 비활성화를 하려면 @Bean(destroyMethod="")
로 설정해줘야 합니다.
매우 편리한 방법이며, 스프링 공식 문서에서도 Best Practice라고 소개하고 있습니다.
또한, 자바 표준 어노테이션이기에 스프링 프레임워크와의 결합도도 낮습니다.
예제 코드는 다음과 같습니다.
@Component
class SampleBean {
private val logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("SampleBean created")
}
@PostConstruct
fun init() {
logger.info("SampleBean PostConstruct")
}
@PreDestroy
fun destroy() {
logger.info("SampleBean PreDestroy")
}
}
생명주기 콜백함수를 통해 스프링 빈을 사용하기 전후에 사용자 정의 코드를 실행시킬 수 있습니다.
스프링은 여기에 더 나아가서 IoC 컨테이너의 스프링 빈의 생성과 의존성 주입에 대해서도 커스터마이징이 가능하도록 기능을 제공하고 있습니다.
이 기능들은 BeanPostProcessor
와 BeanFactoryPostProcessor
인터페이스를 통해 제공되고 있습니다.
BeanPostProcessor
는 IoC 컨테이너가 빈을 생성한 이후에 받는 콜백을 정의할 수 있습니다.
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
두 메소드의 리턴값은 스프링 빈입니다.
BeanPostProcessor
인터페이스는 2개의 메소드를 제공하는데, 앞서 살펴본 빈의 초기화 콜백 메소드 실행 전후로 나뉩니다.
즉, 실행 순서는
1. BeanPostProcessor의 postProcessBeforeInitialization
2. Bean의 @PostConstruct
가 달려있는 메소드
3. BeanPostProcessor의 postProcessAfterInitialization
이 됩니다.
BeanPostProcessor
는 등록된 컨테이너의 모든 빈의 생성에 대해 관여하므로 다른 종류의 빈보다 우선적으로 생성됩니다.
또한, 여러개의 BeanPostProcessor
를 생성할 수 있으며 Order
를 설정하여 순서를 지정할 수 있습니다.
@Component
class SampleBeanPostProcessor: BeanPostProcessor {
private val logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("SampleBeanPostProcessor created")
}
override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? {
if (bean is SampleBean) {
logger.info("SampleBean before initialization")
}
return super.postProcessBeforeInitialization(bean, beanName)
}
override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? {
if (bean is SampleBean) {
logger.info("SampleBean after initialization")
}
return super.postProcessAfterInitialization(bean, beanName)
}
}
위의 SampleBean
과 함께 SampleBeanPostProcessor
를 구현해서 실행시켜보면 다음과 같은 로그가 나옵니다.
2023-04-02T15:42:05.397+09:00 INFO 37338 --- [ main] com.example.core.CoreApplicationKt : Starting CoreApplicationKt using Java 17.0.6 with PID 37338
2023-04-02T15:42:05.399+09:00 INFO 37338 --- [ main] com.example.core.CoreApplicationKt : No active profile set, falling back to 1 default profile: "default"
2023-04-02T15:42:05.820+09:00 INFO 37338 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBeanPostProcessor created
2023-04-02T15:42:05.822+09:00 INFO 37338 --- [ main] com.example.core.SampleBean : SampleBean created
2023-04-02T15:42:05.823+09:00 INFO 37338 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBean before initialization
2023-04-02T15:42:05.823+09:00 INFO 37338 --- [ main] com.example.core.SampleBean : SampleBean PostConstruct
2023-04-02T15:42:05.823+09:00 INFO 37338 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBean after initialization
2023-04-02T15:42:05.853+09:00 INFO 37338 --- [ main] com.example.core.CoreApplicationKt : Started CoreApplicationKt in 0.63 seconds (process running for 0.867)
2023-04-02T15:42:05.855+09:00 INFO 37338 --- [ionShutdownHook] com.example.core.SampleBean : SampleBean PreDestroy
BeanFactoryPostProcessor
는 BeanPostProcessor
와 유사하지만 스프링 빈의 설정 메타정보를 조작하는 데에 차이가 있습니다.
스프링 빈을 생성하기 전에 BeanFactoryPostProcessor
가 설정 메타데이터를 읽고 바꿀 수 있습니다.
BeanPostProcessor
와 마찬가지로 여러개를 생성하여 Order
로 순서를 지정해줄 수 있습니다.
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
@Component
class SampleBeanFactoryPostProcessor: BeanFactoryPostProcessor {
private val logger = LoggerFactory.getLogger(this::class.java)
init {
logger.info("SampleBeanFactoryPostProcessor created")
}
override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory) {
logger.info("Configure beanFactory")
}
}
이번에는 기존의 예제에 BeanFactoryPostProcessor
를 추가해서 실행시켜보겠습니다.
2023-04-02T15:43:22.843+09:00 INFO 37438 --- [ main] com.example.core.CoreApplicationKt : Starting CoreApplicationKt using Java 17.0.6 with PID 37438
2023-04-02T15:43:22.845+09:00 INFO 37438 --- [ main] com.example.core.CoreApplicationKt : No active profile set, falling back to 1 default profile: "default"
2023-04-02T15:43:23.149+09:00 INFO 37438 --- [ main] c.e.core.SampleBeanFactoryPostProcessor : SampleBeanFactoryPostProcessor created
2023-04-02T15:43:23.150+09:00 INFO 37438 --- [ main] c.e.core.SampleBeanFactoryPostProcessor : Configure beanFactory
2023-04-02T15:43:23.161+09:00 INFO 37438 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBeanPostProcessor created
2023-04-02T15:43:23.164+09:00 INFO 37438 --- [ main] com.example.core.SampleBean : SampleBean created
2023-04-02T15:43:23.165+09:00 INFO 37438 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBean before initialization
2023-04-02T15:43:23.165+09:00 INFO 37438 --- [ main] com.example.core.SampleBean : SampleBean PostConstruct
2023-04-02T15:43:23.165+09:00 INFO 37438 --- [ main] c.example.core.SampleBeanPostProcessor : SampleBean after initialization
2023-04-02T15:43:23.196+09:00 INFO 37438 --- [ main] com.example.core.CoreApplicationKt : Started CoreApplicationKt in 0.535 seconds (process running for 0.843)
2023-04-02T15:43:23.199+09:00 INFO 37438 --- [ionShutdownHook] com.example.core.SampleBean : SampleBean PreDestroy
IoC 컨테이너가 생성된 이후에 BFPP가 생성되어 postProcessBeanFactory
가 호출된 이후에 BPP가 생성되는 것을 확인할 수 있습니다.
BeanPostProcessor
는 빈 객체에 대한 조작을 할 때 사용되고,
BeanFactoryPostProcessor
는 빈 메타정보에 대한 조작을 할 때 사용됩니다.
스프링 빈을 프록시로 감싸는 스프링 AOP에서 BeanPostProcessor
를 사용합니다.
AOP말고도 다른 사용처를 살펴보면 AutowiredAnnotationBeanPostProcessor
라는 빈이 있습니다. 해당 빈은 스프링의 @Autowired
나 @Value
어노테이션을 처리하는 빈입니다.
PropertyOverrideConfigurer
의 상속 구조를 따라가다 보면 BFPP를 확인할 수 있습니다. 해당 클래스는 설정 파일의 값을 Bean definition에 설정하는 역할을 담당합니다.
PropertySourcesPlaceholderConfigurer
역시 상속 구조 안에 BFPP가 있습니다. 해당 클래스는 ${...}
로 나타낸 부분을 설정파일이나 환경변수의 값으로 대체해주는 역할을 담당합니다.
스프링 빈의 생명주기 콜백과 컨테이너 확장에 대해 간략하게 알아보았습니다.
이를 이용해, 스프링 빈의 생명주기에서 다양한 시점에서 원하는 코드를 호출하여 커스터마이징을 할 수 있습니다.
Aware 인터페이스에 대해서 다루진 않았지만, 스프링 빈의 생명주기와 깊은 연관이 있으니 Aware 인터페이스도 살펴보는 것을 추천드립니다.