개발놀이터

가상 스레드 (부제 : 자바의 미래) 본문

Java

가상 스레드 (부제 : 자바의 미래)

마늘냄새폴폴 2024. 6. 25. 20:28

이번 포스팅은 가상 스레드에 대해서 알아보도록 하겠습니다. 공부 주제를 선정한 것은 우아한 기술블로그에서 가상 스레드에 대한 포스팅을 보고 나서 결정하게 되었습니다. 

 

글을 되게 재밌게 잘 쓰셨더군요. 글을 보고 흥미가 생겨서 공식 문서를 정독하면서 공부해봤습니다. 

 

거두절미하고 바로 시작해보죠!

 

가상 스레드

기존 자바에서의 스레드는 OS에서 제공해주는 네이티브 스레드를 그대로 사용했습니다. OS와 JVM사이에 JNI라는 인터페이스를 통해 네이티브 스레드를 사용하였죠. 자바 진영에서 이 네이티브 스레드는 플랫폼 스레드라고 부릅니다. 

 

가상 스레드는 가상 메모리와 닮아있는데요. 가상 메모리에선 프로세스가 일을 처리할 때 실제 메모리 주소를 바라보게 하지 않고 가상 주소를 바라보게 함으로써 프로세스 더 나아가 스레드가 일을 처리하면서 메모리의 충돌이 일어나지 않게 합니다. 

 

가상 스레드는 자바에서 존재하는 플랫폼 스레드를 더 잘게 쪼갠 것입니다.

 

가상 스레드를통해 기존에 서버가 처리할 수 있는 처리량 (throughput) 을 기하급수적으로 늘릴 수 있게 되었습니다. 그런데 가상 스레드가 JDK21부터 등장했단말이죠? JDK21은 23년9월에 등장했단말이죠? 처리량을 기하급수적으로 늘릴 수 있는걸 왜 지금서야 만든걸까요? 

 

가상 스레드 탄생 배경

기존 자바의 스레드는 스레드풀을 적절히 사용하면 로직을 처리하는데 큰 문제가 없었습니다. 그만큼 확장성이 높을 필요가 없었던 것이죠. 때문에 자바 언어 개발자들은 스레드를 자바 초창기에 만들어놓고 그대로 두었습니다. 

 

하지만 바야흐로 인터넷 최강 전성기 시대인 2020년대에선 더이상 서버를 증설하는 것으로 I/O 처리량을 늘리기엔 한계가 생길 정도로 엄청난 트래픽을 맞이하는 애플리케이션도 존재합니다. 

 

이렇게 거대한 트래픽을 위해 물리적으로 CPU 코어수를 높이고 메모리를 많이 장착하여 스레드의 수를 늘린다고 한들 부족하게 되었던 것이죠. 

 

또한, 위의 문제와 일맥상통 하는 문제인 스레드의 크기입니다. 어떤 요청은 스레드가 처리하기에 너무 작은 것들도 스레드를 부여받아야 했기 때문에 스레드의 성능을 백퍼센트 끌어올리지 못하는 경우도 있었습니다. 

 

그리고 또! 아무리 스레드가 프로세스에 비해 컨텍스트 스위칭 비용이 낮다고 하더라도 가상 스레드에 비하면 플랫폼 스레드의 컨텍스트 스위칭 비용도 무시하지 못했습니다. 

 

 

가상 스레드 사용하기

가상 스레드를 만드는 방법은 정말 간단합니다. 가상 스레드를 만드는 방법이 두가지 있는데 모두 소개해드리겠습니다. 

 

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

 

첫번째 방법은 Thread 클래스를 사용하는 것입니다. Thread 클래스에서 ofVirtual()메서드를 사용하면 가상 스레드를 사용할 수 있습니다. 

 

Thread의 builder를 활용해서도 시작할 수 있는데 아래와 같습니다. 

 

Thread.Builder builder = Thread.ofVirtual().name("MyThread");
Runnable task = () -> {
    System.out.println("Running thread");
};
Thread t = builder.start(task);
System.out.println("Thread t name: " + t.getName());
t.join();

 

이건 여담이지만 자바 언어 개발자들이 기존 플랫폼 스레드와 호환성을 유지시키기 위해 가상 스레드의 분기를 만들어서 100퍼센트 호환성을 달성할 수 있었습니다. 

 

두번째 방법은 Executor를 이용하는 방법입니다. @Async를 설정할 때 한번씩 보던 Executor를 이용하면 쉽게 사용할 수 있습니다. 하지만 이 방법은 자원을 JVM에 돌려줘야 하기 때문에 오라클에선 try-with-resources를 활용하는 것을 추천하고 있습니다. 

 

try (ExecutorService myExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future = myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
    // ...

 

 

가상 스레드 사용 가이드

이제 주제를 살짝 바꿔서 가상 스레드를 어떻게 사용하면 좋을지 알아보겠습니다. 

 

동기에 블락킹? 그럼 쓰면 좋다!

공식 문서를 읽으면서 아하! 하고 무릎을 탁 쳤습니다. 기존 스레드가 로직을 처리하는데 동기처리이고 블락킹 I/O인 경우 스레드를 다시 스레드 풀에 돌려놓고 다시 사용하기까지 오랜 시간이 걸립니다. 

 

이는 서버의 성능에 악영향을 미칠 수 있는데 가상 스레드는 스레드를 잘게잘게 쪼갠 것이기 때문에 이런 상황에 특화되어 있습니다. 

 

syncrhonized와 잘 안맞는다! pinned 문제

가상 스레드의 동작 방식은 다음과 같습니다. 

 

  1. 플랫폼 스레드에서 떨어져나온다. 
  2. 가상스레드가 생성되고 로직을 처리한다. 
  3. 로직이 완료되면 자원을 반납한다. 
  4. 다시 사용

결국 스레드를 빠르게 사용하고 다시 자원을 반납해야 빠르게빠르게 순환시키면서 가상 스레드의 장점을 백분 활용할 수 있습니다. 

 

하지만 가상 스레드를 사용하면서 synchronized 키워드를 같이 사용한다면 서버의 성능에 문제가 생길 수 있습니다. 

 

일례로 데이터베이스 I/O에선 JDBC 드라이버에 내부적으로 synchronized 키워드가 삽입되어 있어서 그렇게 큰 성능 향상을 얻을 수 없습니다. 

 

 

비동기에선 NO!

만약 자바에서 제공해주는 Future 클래스나 Callable을 이용해서 비동기를 구현했다면 그건 그냥 그대로 냅두는 것이 좋습니다. 왜냐하면 어차피 큰 성능 향상을 이뤄낼 수 없기 때문이죠. 

 

가상 스레드는 동기에 블락킹에 주로 사용하면 될 것 같습니다. 

 

 

ThreadLocal은 최소한으로 사용!

스레드로컬은 스레드가 힙메모리를 공유하면서 사용할 때 벌어지는 동시성 문제를 커버하기 위한 방책중 하나입니다. 가상 스레드는 크기가 작아진만큼 스레드로컬을 최소한으로 사용해야 가상 스레드의 장점을 백분 활용할 수 있습니다. 

 

스레드로컬로인하여 스레드가 무거워지면 가상 스레드를 사용하는 의미가 없어지기 때문입니다. 

 

 

Thread Pool은 사용하지 말것!

플랫폼 스레드는 귀중한 자원이고 만드는데 큰 자원이 소모되기 때문에 한번 만든 것을 버리지 않고 재사용하는 것이 서버의 자원측면에서 이점이 있었습니다. 

 

하지만 기존 플랫폼 스레드가 몇만개정도로 활용되었다면 가상 스레드는 몇백만개까지 생성될 수 있는 일회용품같은 존재입니다. 

 

때문에, 가상 스레드를 그냥 사용하고 GC에 의해 버려지도록 냅두는 것이 성능상 이점을 가져올 수 있습니다. 

 

 

I/O작업은 최고! 하지만 CPU작업은 흠...

가상 스레드는 개발된 취지는 I/O작업에 있어서 현재의 처리량보다 압도적으로 많은 처리를 위해 등장했습니다. 때문에, 개발자가 좋아하는 단어 두가지 응답속도 (latency) 와 처리량 (throughput) 중 처리량에 중점을 둔 만큼 응답속도는 크게 차이가 나지 않습니다. 

 

때문에 CPU작업은 기존 플랫폼 스레드보다 오히려 떨어지는 모습을 보여주기도 하죠. 

 

오라클에선 가상 스레드를 CPU작업말고 I/O작업에 더 적극적으로 쓸 것을 권장했는데요. 아마 가상 스레드가 만능이 아니라는 것을 표현하고 싶었던 것 같습니다. 뚜렷한 목적을 가지고 나왔다는 의미겠죠. 

 

 

번외) 다른 언어에서 가상 스레드

최근 대규모 엔터프라이즈인 네카라쿠배를 중심으로 코틀린이 점점 뜨고 있습니다. 저는 그 이유를 코틀린의 coroutine 때문이라고 생각합니다. 

 

coroutine은 Go lang의 goroutine에서 벤치마킹한 가상 스레드입니다. Go는 GC가 존재하지만 이 goroutine 때문에 같은 계열인 Rust와 성능면에서 견줄 수 있게 되었습니다. 

 

coroutine덕분에 코틀린은 엄청나게 트래픽이 많은 서비스에서 사용되기 안성맞춤이었죠. 하지만 자바밖에 모르는 개발자들이 코틀린을 배워야한다는 러닝커브가 존재했습니다. 

 

자바도 발빠르게 가상 스레드를 추가하면서 코틀린으로 이동을 막으려고 한 것일까요? JDK21부터 추가된 가상 스레드를 적극 추천하는 모습을 보여줬죠. 

 

 

마치며

JDK21만 검색해도 가상 스레드가 연관검색어 최상단에 뜰만큼 굉장히 핫하다고 볼 수 있을 것 같습니다. 이제 훌륭하신 S급 개발자분들이 가상 스레드를 좀 사용해보시고 제가 낙수효과를 좀 받을 수 있기를 기원해봅니다. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

 

 

출처

https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-68216B85-7B43-423E-91BA-11489B1ACA61

 

Core Libraries

Virtual threads are lightweight threads that reduce the effort of writing, maintaining, and debugging high-throughput concurrent applications.

docs.oracle.com

 

'Java' 카테고리의 다른 글

Java8 StreamAPI  (1) 2024.04.28
ConcurrentMap, ConcurrentHashMap  (0) 2023.02.18
상속과 super 그리고 super()  (0) 2022.09.15
final 키워드  (0) 2022.08.04
try-with-resouce  (0) 2022.08.04