가상 스레드(Virtual Thread) 그리고 스프링
1. 가상스레드란?
가상 스레드를 이해하려면 물리 스레드와 가상스레드의 차이점을 이해해야 한다.
1.1 물리스레드
학교 수업시간에 배웠던 스레드 개념이다.
하드웨어 기반
cpu 코어에서 실행되는 스레드다. 각 코어는 한번에 하나의 스레드를 실행할 수 있으며 멀티코어는 동시에 여러 스레드를 실행할 수 있다
시스템 리소스
시스템 리소스를 직접 사용한다. cpu 하나를 독차지 하고 있는 스레드라고 보면 된다. 가상스레드에 대한 개념이 없는 상태에선 이게 무슨 당연한 소리냐 싶을 수 있는데 일단 이렇게만 알고 있자.
계산집약적 작업에 효율적
직접 하드웨어 리소스를 사용하기 때문에 높은 CPU 사용량을 뽑아낼 수 있으므로 계산 집약적 작업에 적합하다
운영체제에서 관리
운영체제에서 관리되므로 컨텍스트 스위칭 비용이 상대적으로 높다. 스레드의 상태 저장, 복원처럼 cpu 레지스트리를 바꿔주는 작업이 OS 단에서 일어난다. OS 를 공부할 때 쓰레드 컨텍스트스위칭이 효율적이라고 배웠을 확률이 높은데 효율적이라는 말은 어디까지나 프로세스와 비교할 때지 가상스레드와 비교했을 때는 비효율적인 작업에 해당한다.
1.2 가상스레드
노드제이에스 이벤트 핸들러 아키텍처나 코틀린의 코루틴을 사용했다면 이미 간접적으로 가상 스레드를 경험한 것이다.
소프트웨어 기반
프로그래밍 언어나 라이브러리에서 구현된 추상화된 스레드다. 즉 가상스레드를 만들어도 어디까지나 프레임워크 단에서만 스레드로 정의되며 운영체제에선 이것을 스레드로 인식하지 않는다.
시스템 리소스
물리시스템과 달리 직접적으로 시스템리소스를 사용하지 않기 때문에 많은 수의 스레드를 생성하고 관리할 수 있다.
효율적인 관리
물리쓰레드보다 훨씬 빠르고 가벼운 컨텍스트 스위칭이 가능하다. 운영체제에서 관리하는 경우 쓰레드 상태 저장/복구 작업이 소요됐던것에 비해서 프레임워크 단에서는 필요한 작업만 수행하도록 최적화했기 때문에 가볍게 수행할 수 있다.
블로킹 작업에 효율적
파일 읽고 쓰기 처럼 I/O 바운드 작업이나 대기시간이 긴 작업에 적합하다. 특정 가상스레드가 블로킹 되면 다른 스레드가 실행될 수 있도록 스케줄러가 제어하기 때문이다.
2. Spring in Virtual Thread
spring 에서 가상쓰레드가 왜 필요할까?
2.1 하드웨어 자원낭비
스프링 같은 경우 요청 마다 별도의 쓰레드를 사용하도록 처리하며 여기서 생성되는 쓰레드는 물리쓰레드다. 하나당 ‘물리 쓰레드를 두는 것이 뭐가 문제냐’ 싶을 수 있는데 가상 스레드의 개념을 알고 나면 요청마다 물리 쓰레드를 두는것이 하드웨어적으로 큰 낭비가 아닐 수 없다.
물리 스레드는 시스템 리소스를 직접 사용하기 때문에 cpu 집약적인 연산을 처리할 때 도움이 된다. 그런데 api 요청이 데이터 베이스로부터 값을 읽고 쓰는 I/O 바운드 작업의 경우 물리 스레드의 이점을 충분히 누릴수가 없다. 오히려 blocking 이 될 때마다 매번 OS 단에서 직접 컨텍스트 스위칭을 처리해줘야하는데 유저모드에서 커널모드를 왔다 갔다 해야하는 낭비만 필요하다.
이런 낭비를 막기 위해선 NodeJS 이벤트 핸들러 아키텍처를 이용해서 네트워크 요청, 파일 I/O 같은 작업을 소프트웨어 단에서 비동기적으로 진행되도록 했다. 운영체제 입장에서는 스레드 하나가 활발하게 실행되고 있는 것으로 보이지만 실제로 node 엔진에서 직접 컨텍스트 스위칭을 해주면서 활발하게 비동기 작업을 처리하고 있다.
2.2 가상스레드 도입
스프링에서는 가상 스레드를 사용해서 위와 같은 단점을 보완하려하고 있다. 11월 16일에 공개된 스프링 블로그 포스트에선 스프링 6.1 버전에서 JDK 21 버전을 채택하고 Virtual Threads 를 사용한다고 했다.
스프링 부트 3.2 버전을 사용하면 spring.threads.virtual.enabled=true 옵션을 주면 Jetty, Tomcat, Kafka, RabbitMQ 에서 사용중인 ExecutorService 를 가상 스레드가 지원되는 ExecutorService(virtual-thread backed ExecutorService) 로 대체해서 사용하고 JAVA21 에서 사용하는 스케줄링 메커니즘으로 관리된다고 한다.
가상 스레드를 사용하는 경우 쓰레드가 Blocked 되는 경우(파일을 읽거나 Sleep 하거나) 런타임에서 Blocking 작업을 감지하고 직접 대기열에 넣어준다. OS 단에서 처리하는 것보다 훨씬 효율적으로 처리할 수 있다.