개요
본 포스팅은 카카오 테크밋에서 안정수님이 발표하신 "JDK 21의 신기능 Virtual Thread 알아보기" 영상을 본 후 글로 정리한 것입니다.
Virtual Thread란 무엇인가
Virtual Thread는 JDK 21에 새롭게 추가된 기능이다. 기존 플랫폼 스레드와는 달리 경량 스레드로써 OS스레드를 그대로 사용하지 않고 JVM 내부 스케줄링을 통해 수십만~수백만개의 스레드를 동시에 사용할 수 있게 해준다.
전통적인 Java의 Thread
- Java의 Thread는 OS Thread를 Wrapping한 것이다. (Platform Thread)
- Java 어플리케이션에서 Thread를 사용하면 실제로는 OS Thread를 사용한 것이다.
- OS Thread는 생성 갯수가 제한적이고 생성, 유지하는 비용이 비싸다.
- 이 때문에 어플리케이션에서는 플랫폼 스레드를 효율적으로 사용하기 위해 Thread Pool을 사용했다.
그렇다보니 처리량에 대한 문제가 발생한다. 스프링은 기본적으로 요청 당 하나의 스레드를 사용하는 Thread Per Request인데 자바의 스레드는 OS 스레드를 Wrapping한 구조이기 때문에 OS Thread를 무한정으로 늘릴 수 없는 단점으로 인해 스레드가 제약이 되고 이 때문에 처리량에 한계에 도달한다.
해결하고자 하는 문제
Virtual Thread가 해결하고자 하는 문제는 첫 번째로 높은 처리량 확보이다. 블락킹이 발생했을 때 내부 스케줄링을 통해 다른 작업을 처리할 수 있게 하면 블락킹 타임동안 스레드를 놀지않게 할 수 있다. 두 번째로 기존 자바 플랫폼의 디자인과 조화를 이루는 코드를 생성하는 것이다. 따라서 아래와 같이 기존 스레드 구조를 그대로 사용한다.
Platform Thread
Platform Thread는 위 사진과 같이 OS Thread와 1:1로 매핑되는 구조를 가지고 있다.
Virtual Thread의 경우 앞단에 Virtual Thread들이 따로 존재한다. 그리고 뒷단에 Fork/Join Pool이 있고 내부에 Carrier Thread가 들어있는데 이 스레드가 위에서 설명한 Platform Thread와 유사한 형태라고 생각하면 된다. 하지만 우리가 명시적으로 Carrier Thread를 사용하는것이 아니고 내부적으로 Virtual Thread가 어떤 Carrier Thread를 사용할 지 결정한다. 어플리케이션에서는 Carrier Thread가 아니라 Virtual Thread만 사용한다. (Carrier Thread는 마찬가지로 OS Thread와 1:1 매핑된다.)
만약 위처럼 Virtual Thread가 특정 Carrier Thread를 사용하던 도중 블락킹이 블락킹이 발생하면 Virtual Thread는 연결된 Carrier Thread에서 Unmount 즉, 연결이 끊기게 된다. 그리고 Carrier Thread는 또 다른 Virtual Thread와 Mount되어 실행하게 된다. 기존에는 IO 블락킹이 발생한 경우 Carrier Thread(Platform Thread)가 기다리기만 했지만 Virtual Thread를 사용하면 블락킹이 발생하는 구간에 있어 Virtual Thread와 Carrier Thread가 Mount/Unmount를 수행함으로써 다른 Virtual Thread 작업을 수행할 수 있게 된다.
이렇게 되면 OS Thread가 개수에 제한이 있는 것에 비해 Virtual Thread는 굉장히 많은 숫자로 늘어나서 사용할 수 있게 된다. 하지만 Virtual Thread의 개수가 많아지는 경우 내부에 있는 데이터를 잘 관리해야하기 때문에 사용하는 자원도 작게 유지해야한다.
사용법
위와 같이 설정하면 내부에서 발생하는 톰캣/WAS에 대한 처리를 Virtual Thread가 수행하도록 해준다.
만약 스프링 부트 3.2 미만 버전이라면 위처럼 직접 빈을 등록해서 사용할 수 있다.
유의 사항
- Virtual Thread는 수백만개까지 늘어날 수 있기 때문에 ThreadLocal을 사용하는 경우 메모리 이슈가 발생할 수 있다.
- synchronized 사용시 Virtual Thread와 연결되어있는 Carrier Thread가 블락킹되기 때문에 주의해야한다. (이런 경우를 pinning이라고 함) 따라서 ReentrantLock을 대신 사용해야 한다.
성능 테스트 1
톰캣 기본 스레드 수가 200이기 때문에 Platform Thread의 경우 해당 값에 근접한 TPS가 도출됐다. 반면 Virtual Thread의 경우 TPS가 3000에 가까운 수치임을 확인할 수 있다. 이는 단순 sleep만 있는 코드이긴 하지만 IO블락킹이 있는 코드에 Virtual Thread를 사용하면 상당히 큰 효과를 얻을 수 있음을 의미한다.
성능 테스트 2
두 번째 성능 테스트는 실제 DB에 쿼리하여 1초를 대기하는 작업이다. Platform Thread의 경우 MySQL의 Max connection이 기본 150이기에 해당 값에 근접하는 TPS를 보여준다.
반면 Virtual Thread는 첫 번째 TPS는 Platform Thread와 동일한 수치를 보여줬지만 나머지 2, 3번째 테스트 시 DB 커넥션 에러가 발생한다.
기존 Platform Thread의 경우 톰캣이 앞단에서 요청을 처리하고 뒤로 넘겨줄 때 가용가능한 스레드가 존재하지 않는 경우(충분히 트래픽을 받지 못하면) 기다려야하는 상황이 발생했지만 Virtual Thread의 경우 톰캣이 앞단의 요청을 모두 소화하고 뒤로 넘겨줬지만 뒷단에서 DB 커넥션을 기다리다가 타임아웃 익셉션이 발생한 것이다. 이말인즉슨 Virtual Thread를 그냥 적용하면 처리량이 좋아지는 상황만을 기대했겠지만 실제로는 좀 더 많은 고민이 필요하다는 것을 의미한다.
적합한 사용처
- IO 블락킹이 발생하는 경우 Virtual Thread가 적합하다.
- CPU Intensive한 작업인 경우 적합하지 않다. (동영상 인코딩 등)
- Spring MVC기반 Web API제공 시 편리하게 사용할 수 있다.
- 높은 처리량을 위해 Webflux를 고려중이라면 대안이 될 수 있다.
Virtual Thread에 대한 오해
- Virtual Thread는 기존(Platform) Thread를 대체하는 것이 목적이 아니고 각자 필요한 부분에 맞게 사용하는 상호 보완적인 관계이다.
- Virtual Thread는 기다림에 대한 개선이다. IO 블락킹이 발생했을 때 Virtual Thread와 연결된 Carrier Thread의 대기 시간을 해소시켜 높은 처리량을 가져오게 하는 것이다.
- 도입한다고 처리량이 무조건 높아지지는 않는다. 예를 들어 웹 소켓/SSE등을 위한 서버를 작성하는 경우 Webflux가 더 나은 선택지가 될 수 있다. 하지만 대부분 Spring MVC 스타일로 코드를 작성하기 때문에 MVC의 경우 적합한 대안이 될 수 있다.
- Virtual Thread는 그 자체로 자바의 동시성을 완전히 개선했다고 보기 어렵다. Virtual Thread는 단순히 기존 Platform Thread가 대기하는 부분을 해소시킨 것이기 때문이다. 따라서 고랭의 고루틴처럼 완전히 새로운 개념을 도입했다고 볼 수 없다.
Virtual Thread의 제약
- Thread Pool에 적합하지 않다. Task별로 Virtual Thread를 할당해서 사용해야한다.
- ThreadLocal 사용 시 메모리 사용량이 급증할 수 있다.
- synchronized 사용 시 Carrier Thread가 블락킹될 수 있기 때문에 주의가 필요하다. (ReentrantLock을 대신 사용)
- 제한된 리소스의 경우 semaphore를 사용하자.
요약
- Virtual Thread가 더 좋은 이유는 기다림에 대한 것
- Virtual Thread는 Platform Thread를 대체하려는 것이 아님, 둘다 사용 가능
- Virtual Thread가 추구하는 것은 "처리량을 증가" 시키면서도 기존 디자인과 조화를 이루는 것
- 처리량 증가는 "리액티브 프로그래밍"과 같지만 보다 가독성이 좋고 개발자 친화적인 방법으로 이를 달성
- Spring Boot 3 에서 기존 코드 스타일 그대로 사용하면서 혜택을 누릴 수 있을 것
- Virtual Thread가 동시성을 완전하게 개선했다고 보기는 어려움(제약이 있다)
'Coding > Java Spring' 카테고리의 다른 글
Java Garbage Collection (0) | 2024.01.30 |
---|---|
스프링 Webflux - 스레드와 이벤트 루프 (0) | 2023.11.13 |
스프링 MVC 1 정리 (0) | 2023.09.12 |
스프링 통합 테스트를 위한 AbstractTestExecutionListener (0) | 2023.08.30 |
Reactive Streams 개념 정리 (0) | 2023.08.23 |