Java21의 Virtual Thread (1)

나민혁·2024년 10월 23일
post-thumbnail

들어가며

최근 프로젝트에서 Java 14에서 추가된 Record도 잘 써먹고 있었고, 코틀린 찍먹을 하면서 알게된 사실로 Java 17에서는 Sealed Class가 추가되었다고 한다.(이건 도대체 어떻게 써먹어야하는지 감이안온다..)

그래서 우테코 프리코스가 Java 21을 사용해야 한다고도 하고, 이젠 LTS가 Java21이니까 Java21에는 뭐가 바뀌었는지 궁금해졌다. 뭐라도 써볼 구석이있을까 해서 한번 뒤적거렸는데 진짜 가상 스레드를 알게 되었다.

이 글을 쓰게 된 계기도 JDK 21의 신기능 Virtual Thread 알아보기 (안정수 James) 영상을 보고나서 정리도 할겸 나도 도전해볼겸 해서 작성하게 되었다.

그리고 영상을 보면서 솔직히 말하면 조금 충격 받았고, CS가 이렇게 다 의미가 있구나 싶으면서, 다음에 하게 될 프로젝트에 적용해보고 싶었다.

그리고 우선 완전한 이해를 바탕으로 글을 작성한게 아닌, 영상과 글을 보고 정리하고, 시도해본 정도이니 참고바란다.

가상 스레드란?

공식문서의 요약을 캡처해서 가져온 부분이다.

Java 플랫폼에 가상 스레드를 도입하는 것은 경량 스레드를 의미하며, 이를 통해 높은 처리량을 요구하는 동시성 애플리케이션을 작성하고 유지하며 모니터링하는 노력을 크게 줄일 수 있습니다.

가상 스레드를 사용하면 높은 처리량을 요구하는 프로그램에서 다양한 이점을 얻을 수 있다는 것 같다.

그래서 가상 스레드의 개념을 이해하기 위해서는 기본적으로 가상 스레드의 탄생 배경에 대해서 알아야한다.

배경

배경에 대해서는 공식문서의 motivation 에서도 참조 할 수 있지만, 유튜브 영상의 발표자님이 정리해주신 블로그를 기반으로 하려고 한다.

1. 자바의 스레드는 OS 스레드를 기본으로 한다.

  • 자바의 전통적인 스레드는 OS 스레드를 랩핑(wrapping)한 것으로 이를 플랫폼 스레드 라고 정의한다. (자바의 전통적인 스레드=플랫폼 스레드)
  • 따라서 Java 애플리케이션에서 스레드를 사용하는 코드는 실제적으로는 OS 스레드를 이용하는 방식으로 동작했다.
  • OS 커널에서 사용할 수 있는 스레드는 갯수가 제한적이고 생성과 유지 비용이 비싸다.
  • 이 때문에 기존에 애플리케이션들은 비싼 자원인 플랫폼 스레드를 효율적으로 사용하기 위해서 스레드 풀(Thread Pool) 만들어서 사용해왔다.

2. 처리량(throughput)의 한계

  • Spring Boot와 같은 애플리케이션의 기본적인 사용자 요청 처리 방식은 Thread Per Request 이다. 이는 하나의 request(요청)을 처리하기 위해서 하나의 스레드를 사용한다.
  • 애플리케이션에서 처리량을 늘리려면 스레드를 늘려야 하지만 스레드를 무한정 늘릴 수 없다. (OS 스레드를 무한정 늘릴 수 없기 때문)
  • 따라서 애플리케이션의 처리량(throughput)은 스레드 풀에서 감당할 수 있는 범위를 넘어서 늘어날 수 없다.

3. Blocking으로 인한 리소스 낭비

  • Thread per Request 모델에서는 요청을 처리하는 스레드에서 IO 작업 처리할 때 Blocking 이 일어난다.
  • 이 때문에 스레드는 IO 작업이 마칠 때까지 다른 요청을 처리하지 못하고 기다려야 한다.(Blocking 동안 대기)
  • 애플리케이션에 유입되는 요청이 많지 않거나 또는 스케일 아웃으로 충분히 커버할 수 있는 정도라면 문제가 없지만,
    아주 많은 요청을 처리해야하는 상황이라면 Blocking 방식으로 인해 발생하는 낭비를 줄여야 할 필요가 있다.
  • 이 때문에 Blocking 이 아니라 Non-blocknig 방식의 Reactive Programming이 발전하였다.
    Reactive Programming의 단점

4. 처리량을 높이기 위한 방법으로 비동기 방식의 Reactive 프로그래밍이 발전해왔다.

  • 한정된 자원인 플랫폼 스레드가 Blocking 되면서 대기하는 데 소요된 스레드 자원을 Non-blocking 방식으로 변경하면서 다른 요청을 처리하는데 사용할 수 있게 되었다.
  • 대표적으로 Webflux 가 이렇게 Non-blocking으로 동작한다.
  • 다만 이런 Reactive 코드는 작성하고 이해하는 비용을 높게 만들었다. (Mono, Flux)
  • 또한 기존의 자바 프로그래밍의 패러다임은 스레드를 기반으로 하기 때문에 라이브러리들 모두 Reactive 방식에 맞게 새롭게 작성되어야 하는 문제가 있다.

5. 자바 플랫폼의 디자인

  • 자바 플랫폼은 전통적으로 스레드를 중심으로 구성되어 있었다.
  • 스레드 호출 스택은 thread local을 사용하여 데이터와 컨텍스트를 연결하도록 설계되어 있다.
  • 이 외에도 Exception, Debugger, Profile(JFR)이 모두 스레드를 기반으로 하고 있다.
  • Reactive 스타일로 코드를 작성하면 사용자의 요청이 스레드를 넘나들면서 처리되는데, 이 때문에 컨텍스트 확인이 어려워져 결국 디버깅이 힘들어졌다.

가상 스레드의 목표

가상스레드의 목표와, 목표하지 않는점에 대해서 나와있다. 지향점과 지양점으로 생각해도 되지않을까? 생각해서 해석하고 정리해보았다.

지향점

서버 애플리케이션이 단순한 요청당 스레드 방식으로 작성되었을 때, 거의 최적의 하드웨어 활용으로 확장할 수 있게 한다.

기존의 java.lang.Thread API를 사용하는 코드가 최소한의 변경으로 가상 스레드를 적용할 수 있게 한다.

기존 JDK 도구를 사용하여 가상 스레드의 문제 해결, 디버깅, 프로파일링을 쉽게 할 수 있도록 한다.

지양점

기존 스레드의 구현 방식을 제거하거나, 기존 애플리케이션을 가상 스레드로 자동으로 마이그레이션하는 것은 목표가 아니다.

Java의 기본 동시성 모델을 변경하는 것은 목표가 아니다.

Java 언어나 Java 라이브러리에서 새로운 데이터 병렬성 구문을 제공하는 것은 목표가 아니다. 대규모 데이터를 병렬로 처리하는 데는 여전히 Stream API가 선호된다.

가상 스레드의 구조

가상 스레드의 구조를 알기 전에 플랫폼 스레드의 구조에 대해서 알아야 한다.

플랫폼 스레드의 구조

이미지 출처 : Virtual Thread란 무엇일까? (1)

플랫폼 스레드를 사용하는 것은 사실은 OS의 스레드를 사용하는 것이다. 이 때 사용하는 스레드는 비용이 비싸기 때문에 스레드 풀 을 사용하여 접근하는 방식으로 사용해왔고 한다.

이미지 출처 : Virtual Thread란 무엇일까? (1)

이에 반해 가상 스레드는 OS 스레드를 감싼 구조가 아니기 때문에 애플리케이션 코드는 가상 스레드 풀 없이 사용하고 JVM 자체적으로 가상 스레드를 OS 스레드와 연결하는 스케줄링한다. 이 작업을 mount / unmount 라고 하고 기존에 플랫폼 스레드라고 하던 부분을 Carrier 스레드라고 한다. (가상 스레드를 실제 OS 스레드로 연결해준다는 의미)

이미지 출처 : Virtual Thread란 무엇일까? (1)

구조적으로 보자면 OS 스레드를 사용하기 전에 하나의 레이어가 더 있는 것 처럼 보인다. (가상 스레드 스케줄링) 하지만 이 자체적인 스케줄링을 통해서 큰 차이가 발생한다. 기존의 스레드는 Blocking 이 발생하면 그냥 기다려야 했는데, 가상 스레드는 Blocking 이 발생하면 내부의 스케줄링을 통해서 실제 작업을 처리하는 Carrier 스레드는 다른 가상 스레드의 작업을 처리하면 된다. 따라서 Non-blocking 이 누리는 장점을 동일하게 누릴 수 있다. 이를 도식화 한 그림이다.

다만 위와 같은 구조는 가상 스레드가 수십~수백만까지 늘어날 수 있기 때문에 전통적인 플랫폼 스레드와 동일한 메모리 비용, 컨텍스트 비용이 발생하면 감당하기 어렵다. 따라서 플랫폼 스레드와 가상 스레드는 자원 사용량의 차이가 있다고 한다.

사용하는 자원의 차이

플랫폼 스레드가상 스레드
메타 데이터 사이즈약 2kb(OS별로 차이있음)200~300 B
메모리 사이즈~2MB~50KB
메모리미리 할당된 Stack 사용필요시 마다 Heap 사용
생성시간~1ms~10µs
컨텍스트 스위칭 시간~100µs~10µs

정리

요약

  1. 가상 스레드는 플랫폼 스레드를 대체하기 위해 나온 것이 아니다. 서로 공존하는 것이다.
  2. 단순한 요청당 스레드 방식으로 작성되었을 때, 거의 최적의 하드웨어 활용으로 확장할 수 있다. ( 무조건 되는 것은 아니다)
  3. 가상 스레드 를 사용하면 Reactive Programming 이 추구하는 Non-blocking을 통한 효율적인 자원 사용이 가능해진다.
  4. 가상 스레드 가 JVM 내부에서 알아서 스케줄링 해주기 때문에 가상 스레드 풀 을 사용하지 않는다.

느낀점

사실은 앞으로 더 글을 쓸 예정이지만, 나에게는 조금 충격을 받은 내용이었다. 그리고 Non-Blocking IO 개념과 Blocking IO에 대해서도 그냥 아~ 그렇구나 라고 들었을뿐이고, 이런걸 어디다쓰지 싶었다. 그런데 발표영상과 블로그를 보고 이런 CS지식이 다 필요하구나 생각했다.

그리고 글을 조금은 많이 참고해서 작성했다. 아니 참고했다기보단, 거의 가져오다싶이 했다. 나만의 언어로 바꾸는게 더 좋았을 것 같다고 생각하지만, 지식이 부족해서인지 바꾸기 쉽지 않았다.

사실은 자바를 쓰고, 스프링을 이용해서 개발해왔지만 이런방식으로 아랫단에서 스레드가 어떻게 동작하는지 알지 못했다. 애초에 플랫폼 스레드라는 개념을 알지도 못했고, OS의 스레드를 그대로 쓴다는 것도 몰랐다.

그리고 이 글을 쓰게 된 계기가 된 JDK 21의 신기능 Virtual Thread 알아보기 (안정수 James) 영상의 댓글에서 이러한 내용을 알게 되었다.

이미 Go의 고루틴, Kotlin의 코루틴 등에서 많이 쓰이던 개념인 것 같다. 그리고 기반지식으로 CS의 Continuation에 대해서 공부해봐야겠다고 생각했다. 대부분의 설명이 코틀린의 코루틴으로 설명하고있다. 그래서 Kotlin인게 조금은 장벽이긴하지만.. 공부를 열심히 해야겠다....ㅎㅎ

그래도 가상 스레드에 대해서 어느정도 이해하게 되었고, 적용시킬 수 있는지 확인해보고 적용시켜 나가볼 예정이다.

다음 글에서는 자바에서 가볍게 사용하는 방법을 알아보고, 실제 스프링에서 가상스레드를 확인해보는 작업을 해볼 것이다.


참고자료
JDK 21의 신기능 Virtual Thread 알아보기 (안정수 James)
Virtual Thread란 무엇일까? (1)
Virtual Thread란 무엇일까? (2)
JEP 444: Virtual Threads
[4월 우아한테크세미나] ‘Java의 미래, Virtual Thread’

0개의 댓글