본문 바로가기
허브 살리기 프로젝트

Spring Boot Warm Up

by jay-choe 2024. 4. 17.

Warm Up

Spring Boot 어플리케이션을 배포하게 되면 첫 요청에 대한 응답은 상대적으로 느리고 이후 요청부터 점점 테스트 했던 지연시간 만큼 줄어드는 걸 확인 할 수 있다. 원인은 JVM의 클래스 로딩 방식이 Lazy 한 것과 Spring Boot 내부에서 사용하는 Bean들 중 지연 로딩을 적용 한 것들이 실제 요청을 받을 때 생성하기 때문에 그만큼 응답 시간에 영향을 미치기 때문이다.


그렇다면 원인을 파악했으니 지연 로딩되는 클래스들과 Bean들을 첫 요청 전에 로딩 시켜 놓으면 첫 요청이 느린 문제를 해결 할 수 있다.
이렇게 사전에 준비를 해놓는 것을 'Warm Up'이라고 부른다.


(물론 JVM JIT Compiler cache - Tiered Compilation 을 통한 최적화 하는 부분도 다른 글에서 읽어봤는데 해당 최적화를 할 만큼 낮은 지연 시간 - 20ms 이내 같은.. 서비스를 만들어 본 적은 없으며 해당 최적화는 첫 요청이 느린 이유랑 큰 영향이 없다는 것을 확인했다)

 

Warm Up이 필요한 상황

서비스의 요건을 맞추기 위해서 낮은 지연시간을 요구하는 API들이 존재 할 것이다.

이런 API들이  배포 할 때마다 한두건씩 요청에 대한 응답이 느리다면 운이 없는 사용자들은 안 좋은 경험을 하게 될 것이다.

 

물론 앞단에서 특정 시간이 넘어가면 요청을 끊는 경우도 있다.

 

그렇게 된다면 연동 된 서비스들은 에러 알람을 받게 될 것이며 새로운 배포는 부담이 된다는 인식을 갖게 될 것이다.

 

이런 경우 미리 Warm Up을 진행 해 놓는다면 부담 없이 배포를 할 수 있을 것이다.

 

Spring Boot에서의 Warm Up

보통 2가지 방식으로 하는 것으로 보인다.

  • ApplicationReadyEvent
    해당 이벤트를 받는 Listener를 Bean으로 등록하고 여기서 어플리케이션에서 사용 할 관련 Bean을 호출을 해주는 방식이다.

 

해당 이벤트의 자바독을 읽어보면 요청을 처리할 수 있을 시점에 발행되는 이벤트라고 한다.

 

코드는 다음과 같이 사용할 수 있다.

 

@Component
class ApplicationReadyListener: ApplicationListener<ApplicationReadyEvent> {

  override fun onApplicationEvent(event: ApplicationReadyEvent) {
    // 로직 작성
  }
}

 

 

  • CommandLineRunner
    마찬가지로 해당 인터페이스를 구현하는 구현체를 Bean으로 등록한다.

@Component
class StartRunner: CommandLineRunner {

  override fun run(vararg args: String?) {
	// 로직 작성
  }
}

 

 

여러 글들을 보면 두 방식중 아무거나 써도 될 것 같다.

 

순서

두 방식의 순서는 어떻게 될까?

간단히 print를 찍어보면 ApplicationEvent가 나중에 처리가 되는 것을 확인 할 수 있다.

 

SpringApplication 클래스에서 run 함수를 따라가보면 callRunners를 호출하고 이후에 ready Event를 처리하는 것을 확인 할 수 있다.

 

 

요청 받는 시점

저 두 방식으로 처리를 하는 것은 알겠다. 그러면 중요한 것은 요청 받는 시점에 Warm Up이 되어 있어야 하는 것인데.

요청 자체는 언제 받을까? 물론 WebServer가 실행되어 요청을 받을 수 있는 시점부터 요청을 받을 수는 있을 것이다.

 

WebServerInitializedEvent의 리스너를 등록하고 Thread Sleep을 길게 주고 요청을 보내보면 응답이 오는 것을 확인 할 수 있다.

물론 이 이벤트는 위의 언급한 Warm Up을 처리하는 이벤트 / 함수 호출 보다 이전에 발생 된다.

 

그렇다면 Warm Up 도중에 요청이 들어 올 수 있다는 것을 의미한다.

 

이벤트 처리가 동기식으로 동작을 해도 요청을 받는 톰켓은 별도 쓰레드로 처리가 되기 때문에

 

@Component
class ApplicationReadyListener: ApplicationListener<ApplicationReadyEvent> {

  override fun onApplicationEvent(event: ApplicationReadyEvent) {
    println("Hello From ready Listener")
    Thread.sleep(20000)
    println("Bye From ready Listener")
  }
}

 

이런식으로 Sleep을 주고 확인 해 봐도 요청을 처리하는 것을 확인 할 수 있다. 하지만 요즘의 무중단 배포 방식을 생각해보면 이것은 중요하지 않다는 것을 알 수 있다.

 

트래픽 전환과 Warm Up

요즘 배포 방식은 충분한 Health Check의 임계값을 넘기고 트래픽이 전환되는 방식이기 때문에 트래픽 전환 이전에 Warm Up 단계가 끝날 것이다.

 

물론 Warm Up 시간이 오래 걸린다면..

 

어플리케이션 레벨이 아닌 CI/CD 쪽에서 트래픽 전환 텀을 충분히 길게 두거나 다른 방식 (트래픽 전환 할 때 확인 할 API를 별도로 만든다던지..)이 필요할 것이다.