한 줄 요약
spring.threads.virtual.enabled=true 를 한 경우에는 spring.main.keep-alive=true 도 설정해야 할지 검토하세요
Java 21에서 Virtual Thread(이하 가상 스레드)가 추가됨에 따라 Spring Boot 3.2.x부터 이를 사용하도록 설정할 수 있게 됐습니다. 속성 파일에서 spring.threads.virtual.enabled 를 true 로 설정하면 내장 WAS, @Async가 붙은 메소드 처리, 스케줄러 등에서 플랫폼 스레드가 아닌 가상 스레드를 사용하게 됩니다. Spring Boot에서 가상 스레드를 활성화할 때는 기본적으로 고정된 가상 스레드(pinned virtual thread)와 관련된 성능 측면을 고려해야 하지만, 그 외에 한 가지 주의할 점이 있습니다. 그 내용을 간단히 살펴보겠습니다.
주의해야 할 사항
가상 스레드는 데몬 스레드입니다. JEP 444에서 규약으로 정해진 내용이며, 데몬 속성을 false로 변경할 수도 없습니다. JVM은 모든 스레드가 데몬 스레드인 경우에는 종료하게 됩니다. 그래서, Spring Boot 기반의 애플리케이션의 지속적인 실행이 @Scheduled 같은 빈에 의존하는 경우 가상 스레드 사용을 활성화하면 애플리케이션은 계속 실행되지 않고 종료됩니다. 다시 얘기하면, 임베디드 WAS를 실행하지 않는 환경에서 1회 실행으로 종료되지 않고 지속적으로 실행되는 애플리케이션에서는 해당 지속성이 가상 스레드에 의해서 이루어지는 경우 원하는 대로 유지되지 않고 종료됩니다.
예시
application.yml
spring:
threads:
virtual:
enabled: true
build.gradle 파일의 의존성 부분
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
....
}
web 스타터가 없습니다. 즉, 임베디드 WAS가 실행되지 않습니다.
@EnableScheduling
@EnableScheduling
@SpringBootApplication
class VirtualthreadApplication
JobScheduler
@Component
class JobScheduler {
@Scheduled(fixedRate = 5000L)
fun work() {
printThreadInfo()
}
private fun printThreadInfo() {
val t = Thread.currentThread()
println("<<Current Thread Info>>")
println("Thread ID: ${t.threadId()}")
println("Daemon: ${t.isDaemon}")
println("Virtual: ${t.isVirtual}")
}
}
테스트를 위해 임의로 만든 클래스입니다.
이렇게 구성하여 주기적으로 JobScheduler#work가 실행되는 Spring Boot 기반의 애플리케이션을 구성했다고 했을 때, 이 애플리케이션을 실행하면 5초 단위로 work가 계속 실행되지 않고, 다음과 같이 애플리케이션이 종료됩니다.
로그 레벨이 ERROR보다 높거나 특정 상황에 따라 한 번 정도 실행이 될 수도 있지만, 어쨌든 지속적으로 실행되지 않고 바로 종료됩니다. 이유는 @Scheduled 애노테이션이 있는 스프링 빈 유지를 위해 가상 스레드가 사용되고, 현재 앱에는 이 스레드 밖에 유지되는 스레드가 없기 때문입니다. 가상 스레드는 데몬 스레드이고 애플리케이션의 모든 스레드가 데몬 스레드이면 JVM은 종료됩니다.
해결 방법
spring.main.keep-alive 속성을 true로 설정하면 이 문제를 해결할 수 있습니다.
spring:
threads:
virtual:
enabled: true
main:
keep-alive: true
이렇게 속성을 추가하고 실행을 하면 애플리케이션이 종료되지 않고 원하는 대로 잘 실행됩니다.
해결이 되는 이유
spring.main.keep-alive 속성 값이 true면 Spring Boot는 내부적으로 데몬 속성이 아닌 스레드를 하나 생성 및 유지합니다. 그렇게 해서 모든 스레드가 가상 스레드인 상황을 피합니다. 관련된 코드는 다음의 링크에서 확인할 수 있습니다.
SpringApplication.java
- spring.main.keep-alive 속성 값을 확인하고 KeepAlive 클래스를 인스턴스화하는 부분: 코드 보기
- KeepAlive - Application Context 생명 주기에 따라 데몬이 아닌 스레드를 실행 및 종료시키는 클래스: 코드 보기
SpringApplication 클래스 내부에 정의돼 있는 KeepAlive 클래스 코드는 다음과 같은 형태입니다.
private static final class KeepAlive implements ApplicationListener<ApplicationContextEvent> {
private final AtomicReference<Thread> thread = new AtomicReference<>();
@Override
public void onApplicationEvent(ApplicationContextEvent event) {
if (event instanceof ContextRefreshedEvent) {
startKeepAliveThread();
}
else if (event instanceof ContextClosedEvent) {
stopKeepAliveThread();
}
}
private void startKeepAliveThread() {
Thread thread = new Thread(() -> {
while (true) {
try {
Thread.sleep(Long.MAX_VALUE);
}
catch (InterruptedException ex) {
break;
}
}
});
if (this.thread.compareAndSet(null, thread)) {
thread.setDaemon(false);
thread.setName("keep-alive");
thread.start();
}
}
private void stopKeepAliveThread() {
Thread thread = this.thread.getAndSet(null);
if (thread == null) {
return;
}
thread.interrupt();
}
}
Application Context가 준비된 단계에 발생하는 ContextRefresh 이벤트에 데몬이 아닌 스레드를 생성하고, 종료되는 시점에 발생하는 ContextClosed 이벤트에 해당 스레드를 종료합니다. 코드를 보시면 스레드의 데몬 속성을 명시적으로 false로 지정하는 부분도 보실 수 있을 겁니다.
여담으로 이 문제에 대한 논의는 다음의 링크에서 진행된 것으로 보입니다. aahlenst라는 GitHub 사용자가 문제를 제기했고, 이를 검토하여 받아들인 것으로 보입니다.
https://github.com/spring-projects/spring-boot/issues/37736
Add 'keep-alive' property to SpringApplication and document that it is useful when using virtual threads · Issue #37736 · spri
With platform threads, a method annotated with @Scheduled keeps an empty Spring Boot application running. When virtual threads are enabled, this is no longer the case and the application stops once...
github.com
참고
'Tips' 카테고리의 다른 글
FastAPI에서 openai의 stream을 text/event-stream으로 반환하기 (1) | 2024.02.25 |
---|