스프링부트에서 무조건 한번 실행하게 하기
서버 개발 중 스프링 부트가 실행 될 때, 반드시 한번 실행 시키고 싶은 코드가 생겼다. 코드 상 서비스로직에 추가해야할 것 같고, 굳이 빈일 필요가 없다.
예를 들어, 변수에 어떤 값을 초기화 한다던가, 객체를 초기화 한다던가 여러가지 예가 있을 것 같다.
여러가지 방법이 있다. 하나씩 정리해보자.
1. @PostConstruct
@Service
class DataService {
private val dataList = mutableListOf<String>()
@PostConstruct
fun loadData() {
// 애플리케이션 시작 시 데이터 초기화
dataList.addAll(listOf("Data1", "Data2", "Data3"))
println("초기 데이터 로드 완료: $dataList")
}
}
일반적으로가 가장 많이 알려진 방법이지 싶다. 그러나 스프링부트 3으로 넘어오면서 사용은 가능하지만 권장하지는 않는다.
1. Spring이 자체적으로 더 나은 라이프사이클 관리를 제공한다. 아래에서 정리할 계획..
2. 스프링부트 3.0 버전에서는 javax.annotation 패키지가 jakarta.annotation으로 변경되었다. jakarta.annotation 패키지도 @PostConstruct를 제공하지만, 기존 패키지에서 사용하던 방식을 권장하지 않는다.
3. @PostConstruct는 모든 빈이 주입되기 전에도 실행될 가능성이 있음. 특히 Spring의 AOP(proxy)와 충돌 가능성이 있으며, 일부 테스트 환경에서 예상치 못한 동작을 할 수 있다.
@Service
@Transactional
class MyService {
@PostConstruct
fun init() {
println("PostConstruct 실행됨") // ❌ AOP 프록시가 적용되면서 실행되지 않을 가능성 존재
}
}
3번이 가장 치명적인데, 아직 나는 경험해본적이 없다. 변수가 적은 프로젝트에서라면 사용해도 문제 없는 것 같다.
2. init Block
다음은 init 블록인데, 이건 서비스 로직이 있는 빈에서 사용을 별로 권장하지 않는다.
@Component
class MyService {
init {
println("MyService 빈 생성됨")
println("MyService 초기화 - repository 사용 시도: ${repository.findAll()}") // ⚠️ 오류 발생 가능
}
}
위의 예시처럼 init 블록 내에서 다른 빈을 호출한다면 빈이 주입되기 전이라 오류가 발생할 가능성이 있다. 그러나 정적 데이터를 관리하기 위한 용도로는 사용하기 좋다.
3. @Configuration
서버 어플리케이션을 시작할 때 무조건 한번 실행되고, 싱글톤 빈을 띄워 주는 어노테이션이다.
@Configuration
class AppConfig {
@Bean
fun myService(): MyService {
return MyService()
}
}
무조건 한번 실행시키는데는 적합한데 @Configuration은 서비스로직을 관리하기 위한 어노테이션이 아니다.
서비스 로직을 넣는다면 정상적으로 동작하겠지만 스프링 빈의 생명주기엔 적합하지 않은 방식의 구현이기 때문에, 잘 생각해서 사용해야 한다.
용도를 정확히 기억해서 사용하자 : @Configuration은 설정 및 초기화 용도이다.
4. Runner (권장)
CommandLineRunner
@Component
class MyCommandLineRunner : CommandLineRunner {
override fun run(vararg args: String?) {
println("🚀 CommandLineRunner 실행됨!")
println("전달된 인자: ${args.joinToString()}")
}
}
ApplicationRunner
@Component
class MyApplicationRunner : ApplicationRunner {
override fun run(args: ApplicationArguments) {
println("🚀 ApplicationRunner 실행됨!")
println("옵션 인자: ${args.optionNames}")
}
}
CommandLineRunner, ApplicationRunner를 사용하고, 각 Runner의 인터페이스를 상속 받아서 사용하면 끝이다.
run 매서드를 반드시 한번 실행해주는데, 각각 커맨드라인 파라미터를 핸들링하는 방식의 차이가 있지 동작은 거의 동일하다.
스프링부트의 빈 생명 주기를 지키면서, 서비스 로직에서 사용을 권장하는 방식이다.
5. InitializingBean (권장)
@Service
class MyInitializingBeanService : InitializingBean {
override fun afterPropertiesSet() {
println("🚀 InitializingBean 실행됨 - 빈 초기화 완료!")
}
}
afterPropertiesSet() 메서드는 의존성 주입이 완료된 후 실행된다. @Service, @Component 등으로 등록된 빈이 초기화될 때 자동으로 실행됨. @PostConstruct와 유사하지만, Spring Boot 3에서도 권장되는 방식이다.
6. @EventListener(ApplicationReadyEvent::class) (권장)
@Component
class StartupListener {
@EventListener(ApplicationReadyEvent::class)
fun onApplicationReady() {
println("🚀 애플리케이션이 완전히 실행된 후 실행됨!")
}
}
@EventListener(ApplicationReadyEvent::class)는 애플리케이션이 완전히 실행된 후 실행할 코드가 필요할 때 사용하면 된다.
EventListener 역시 스프링이 제공하는 생명 주기를 잘 따르기 때문에 권장하는 방식이다.
마치며
그동안 나도 웬만하면 @PostConstruct를 썼었다.
그러다 러너를 알게되고 어떤 방식이 올바른 방식인지 고민하다 이번 글이 나오게 됐다.
본인이 스프링 부트 3.0 이상 버전을 사용한다면 스프링에서 자체적으로 제공하는 생명주기를 잘 따르는 Runner, InitializingBean, EventListener를 사용하는 것을 권장한다.
스프링 생명주기 관리도 지원하면서, 가독성도 좋기 때문에 추천하는 방식기 때문이다.