1. 글을 작성하게 된 계기
스프링을 사용하며 특정 시점에 작업을 처리 할 일이 있었는데, 이때까지 사용했던 방법과, 새로 알게 된 내용을 한 번에 정리하기 위해 글을 작성하게 되었습니다.
특정 시점은 스프링의 특정 빈이 초기화 되었을 때, 애플리케이션이 구동되었을 때와 같은 시점을 말합니다.
2. 라이프 사이클
2-1. ApplicationRunner
ApplicationRunner를 구현 상속하면 스프링 애플리케이션이 구동된 직후, 즉, 모든 빈들이 초기화 된 후 특정 작업을 수행할 수 있습니다.
1
2
3
4
5
6
7
8
9
@Component(value = "startLog")
class StartLogConfig: ApplicationRunner {
private val log = logger()
override fun run(args: ApplicationArguments?) {
log.info("Hello World.")
}
}
만약 다른 클래스에서도 ApplicationRunner을 구현 상속하고 있다면 @DependsOn 으로 실행 순서를 제어할 수 있습니다.
1
2
3
@DependsOn("startLog")
@Component(value = "startLogV2")
class StartLogConfigV2 : ApplicationRunner
2-2. @PostConstruct
@PostConstruct를 사용하면 특정 빈이 생성된 후, 초기화 작업이 끝나고 IOC 컨테이너에 등록되면 이를 호출합니다. 이는 단일 빈의 생명주기 에 초점을 맞추기 때문에 ApplicationRunner와 함께 사용하면 @PostConstruct가 먼저 호출됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component(value = "startLog")
class StartLogConfig: ApplicationRunner {
private val log = logger()
@PostConstruct
fun initLogAfterBeanInit() {
log.info("[1] Init.")
}
override fun run(args: ApplicationArguments?) {
log.info("[2] Hello World.")
}
}
한 클래스에서 여러개의 @PostConstruct를 사용할 수도 있는데, 이 경우 메서드 위치 순서 대로 메서드를 호출합니다. 즉, 위에서 부터 아래로 메서드가 실행됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Order(2)
@Component(value = "startLog")
class StartLogConfig{
private val log = logger()
@PostConstruct
fun initLogAfterBeanInitV1() {
log.info("[1-1] Init.")
}
@PostConstruct
fun initLogAfterBeanInitV2() {
log.info("[1-2] Init.")
}
@PostConstruct
fun initLogAfterBeanInitV3() {
log.info("[1-3] Init.")
}
......
}
1
2
3
INFO 79248 --- [main] p.lifecycle.app.common.StartLogConfig : [1-1] Init.
INFO 79248 --- [main] p.lifecycle.app.common.StartLogConfig : [1-2] Init.
INFO 79248 --- [main] p.lifecycle.app.common.StartLogConfig : [1-3] Init.
그러나 다른 빈에서도 @PostConstruct를 사용중이라면 메서드 단위로 실행 순서를 보장할 수 없습니다.
1
2
3
4
5
6
7
8
9
10
11
@Order(1) // 순서 제어 불가
@Component
class OtherPostConstruct {
private val log = logger()
@PostConstruct
fun otherInit() {
log.info("[Other] Init")
}
}
이 경우 마찬가지로 @DependsOn 을 사용해 어떤 빈이 먼저 등록될 지, 순서를 정해줘야 합니다. 즉, @PostConstruct는 동일 클래스 내 실행 순서를 보장** 할 수 있지만, 다른 클래스에서 이를 사용할 경우, 메서드 단위로 실행 순서를 보장할 수 없습니다.
1
2
3
@DependsOn(value = ["otherPostConstruct"])
@Component(value = "startLog")
class StartLogConfig
2-3. InitializingBean
InitializingBean을 구현상속하면 InitializingBean은 스프링 프레임워크에서 제공하는 인터페이스로, afterPropertiesSet() 메서드를 오버라이딩하여 빈의 모든 프로퍼티가 설정된 후에 커스텀 초기화 작업을 수행할 수 있습니다. 이 인터페이스를 사용하면 다음과 같은 특징이 있습니다:
1
2
3
4
5
6
7
8
9
@Component(value = "startLog")
class StartLogConfig: InitializingBean {
private val log = logger()
override fun afterPropertiesSet() {
log.info("[2] afterPropertiesSet")
}
}
InitializingBean 인터페이스 InitializingBean은 스프링 프레임워크에서 제공하는 인터페이스로, afterPropertiesSet() 메서드를 오버라이딩하여 빈의 모든 프로퍼티가 설정된 후에 커스텀 초기화 작업을 수행할 수 있습니다. 이 인터페이스를 사용하면 다음과 같은 특징이 있습니다:
프로그래매틱: 초기화 로직을 Java 코드 내에서 명시적으로 구현해야 합니다. 확장성: InitializingBean 인터페이스를 구현하는 것은 클래스의 계층구조 내에서 활용할 수 있으며, 초기화 로직을 상속받은 클래스에서 재사용할 수 있습니다. 스프링 종속성: 이 인터페이스는 스프링 프레임워크에 종속적이므로, 스프링 외의 다른 컨테이너에서는 사용할 수 없습니다. 실행 시점의 차이 @PostConstruct: 종속성 주입이 완료된 후 즉시 호출됩니다. InitializingBean: afterPropertiesSet() 메서드는 @PostConstruct 어노테이션이 지정된 메서드 이후에 호출됩니다. 즉, @PostConstruct는 afterPropertiesSet()보다 먼저 실행됩니다. 사용 시 고려 사항 @PostConstruct가 권장되는 방식입니다. 그 이유는 위에서 언급한 것처럼 간결하고 선언적이며 자바 EE 표준을 따르기 때문입니다. 그러나 InitializingBean 인터페이스를 사용해야 하는 특정 경우도 있을 수 있습니다. 예를 들어, 빈의 초기화 로직을 상속 계층을 통해 전파하고자 할 때나 빈 후처리기와의 상호 작용을 보다 세밀하게 제어해야 할 때 등입니다.
어떤 방식을 사용할지는 애플리케이션의 요구사항과 개발자의 선호도에 따라 결정됩니다. 일반적으로 스프링에서는 @PostConstruct를 사용하는 것을 더 선호합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component(value = "startLog")
class StartLogConfig: BeanPostProcessor {
private val log = logger()
@PostConstruct
fun initLogAfterBeanInit() {
log.info("[1] Init.")
}
override fun afterPropertiesSet() {
log.info("[2] HELLO")
}
override fun run(args: ApplicationArguments?) {
log.info("[3] Hello World.")
log.info("Env is:$env")
}
override fun destroy() {
log.info("[4] GoodBye.")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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
}
}
2-5. DisposableBean
DisposableBean을 구현 상속하면 스프링 애플리케이션이 종료되는 시점에 특정 작업을 수행할 수 있습니다. 즉, 애플리케이션을 종료할 때, destroy 메서드를 실행하는 것입니다.
1
2
3
4
5
6
7
8
9
@Component(value = "startLog")
class StartLogConfig: DisposableBean {
private val log = logger()
override fun destroy() {
log.info("[4] GoodBye.")
}
}
@Bean 초기화 메서드
1
2
3
4
5
6
7
8
9
10
11
12
class ExampleBean {
private val log = logger()
fun initExampleBean() {
log.info("[ExampleBean] Init.")
}
fun destroyExampleBean() {
log.info("[ExampleBean] Destroy.")
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Profile("!test")
class ExampleConfig {
private val log = logger()
@Bean(
initMethod = "initExampleBean",
destroyMethod = "destroyExampleBean"
)
fun exampleBean(): ExampleBean {
log.info("[Profile] Init.")
return ExampleBean()
}
}
3. 정리
스프링 부트 3.2 버전 이후, 잘못된 URL로 요청이 왔을 때, 어떻게 커스텀한 에러 응답을 내려줄 수 있는지 살펴보았습니다. 방법 자체는 간단한데요, 추가로 @EnableWebMvc와 DispatcherServlet에 대해 학습해 보실 것을 권장해 드립니다.