개발/SPRING

스프링부트에서 무조건 한번 실행하게 하기

애쿠 2025. 3. 7. 21:52

서버 개발 중 스프링 부트가 실행 될 때, 반드시 한번 실행 시키고 싶은 코드가 생겼다. 코드 상 서비스로직에 추가해야할 것 같고, 굳이 빈일 필요가 없다. 

 

예를 들어, 변수에 어떤 값을 초기화 한다던가, 객체를 초기화 한다던가 여러가지 예가 있을 것 같다.

 

여러가지 방법이 있다. 하나씩 정리해보자.

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를 사용하는 것을 권장한다.

 

스프링 생명주기 관리도 지원하면서, 가독성도 좋기 때문에 추천하는 방식기 때문이다.