지난달 Final RC 가 끝난 Java 21 은 9월 19일에 출시된다. 새로운 기능 중 이번에 정식으로 제공되는 Virtual Thread 에 대해서 알아봤다.(19,20 에서 preview 기능으로 제공)
10개의 요청을 동시에 처리하여 초당 200개의 요청을 처리할 수 있는 애플리케이션이 있다고 가정해 보자. 사용자 수가 늘어 초당 2,000개의 요청을 처리해야 한다면 100개의 요청을 동시에 처리할 수 있도록 해야 한다. 그러면 서버는 스레드 수를 늘려 문제없이 요청을 처리할 수 있다. 나중에 사용자수가 더 늘어 초당 20,000개의 요청을 처리해야 하는 경우 마찬가지로 서버는 스레드 수를 늘려 문제없이 요청을 처리할 수 있을까? 아쉽게도 그럴 수 없다. 자바에서 생성할 수 있는(polling 할 수 있는) 스레드 수는 한계가 있다. 그 이유는 OS 스레드를 기반으로 1:1 대응되어 만들어져서 생성할 수 있는 스레드 수가 서버 하드웨어에 한정되기 때문이다.
그렇다면 어떻게 처리량을 늘릴 수 있을까?
자바 애플리케이션은 기본적으로 하나의 요청을 하나의 스레드가 처리한다. 즉 요청을 처리하면서 네트워크 I/O, 파일 I/O 등과 같은 다른 I/O 작업이 발생하면 해당 스레드는 blocking 된다.(외부 작업이 끝날 때까지 자바 스레드는 대기하게 된다) 이런 대기 상태의 스레드를 다시 새로운 요청을 받을 수 있도록 하면 한정된 하드웨어를 최대한 활용해서 처리량을 높일 수 있다. 하지만 이 방법에는 문제점이 있다.
애플리케이션의 처리량을 늘리는 동시에 위와 같은 문제점을 해결하고자 JDK 개발자들은 가상 스레드를 만들었다. 가상 스레드는 플랫폼 스레드(기존의 자바 스레드)와 다르게 OS 스레드와 연결되어 있지 않다. 즉 가상 스레드는 일반적인 인스턴스이기 때문에 생성 비용이 저렴하다.
때문에 요청을 처리하는 도중 다른 I/O 작업이 발생하면 해당 가상 스레드는 blocking 되고 새로운 가상 스레드 객체를 만들어 다음 요청을 처리한다.
그리고 요청을 처리하는 도중 CPU 에서 계산을 수행하는 동안에만 플랫폼 스레드를 사용한다. 즉 플랫폼 스레드 측에서 봤을 때 1개의 플랫폼 스레드를 N 개의 가상 스레드가 사용한다.(둘을 같이 놓고 보면 많은 수(M)의 가상 스레드가 더 적은 수(N)의 OS 스레드에서 실행된다)
비유하자면 물리주소는 메모리 크기에 한정되지만 CPU 가 사용하는 논리주소는 메모리 크기에 한정되지 않는 것처럼(메모리 + 스왑영역) OS 스레드는 하드웨어(메모리 + 스왑영역)에 의해 한정되지만, 자바 런타임의 가상 스레드는 OS 스레드에 한정되지 않는다.
Virtual Thread 를 사용하면 OS 스레드를 blocking 없이 효율적으로 활용해서 애플리케이션의 처리량을 늘릴 수 있으며, Virtual Thread 는 요청과 1대1로 대응되기 때문에 프로그램 흐름을 쉽게 파악할 수 있다는 이점이 있다.