티스토리 뷰

최신 프로젝트는 현재 기준 최신 LTS 버전인 JDK 21 에서 개발하고 있다. JDK 21 에 추가된 기능 중 하나는 JEP(Java Enhancement Proposal) 444 에 포함된 virtual thread 이다.

 

virtual thread 에 대해 간략하게 설명하면 OS 스레드와 별개로 JVM 수준에서 한번 더 추상화한 스레드를 만들고, 이를 OS 스레드와 연결(mount 라고 표현한다)해서 멀티 스레드 프로그래밍을 하도록 하는 개념이다. virtual thread 는 OS 스레드와는 독립적인 라이프 사이클을 갖게되며 virtual thread 를 생성한다고해서 꼭 OS 스레드를 추가로 생성하진 않는다.

 

기존 JVM 에서는 JVM 수준의 스레드인 플랫폼 스레드라는 개념이 있었지만 이 플랫폼 스레드는 항상 OS 스레드와 1:1 로 매핑이 되는 관계였다. 때문에 플랫폼 스레드를 추가 생성한다는 것은 곧 OS 스레드를 추가 생성하는 것이고, 이 때문에 스레드를 많이 생성하는 것 자체가 리소스에 부담이 되는 경우가 많았다.

 

virtual thread 는 OS 스레드와 1:1 매핑이 아니기 때문에 스레드 추가 생성에 대한 부담이 매우 낮아졌고, 이 때문에 virtual thread 는 굳이 스레드 풀을 만들어서 사용하는걸 권하지도 않는다. IO 작업이 빈번한 경우 virtual thread 를 생성해서 실행하면 JVM 이 알아서 적절한 플랫폼 스레드를 통해 task 를 실행한다.

 

개인적으로 virtual thread 에 많은 기대를 하고 있는 편이다. 다만 virtual thread 를 사용할때 동기화 방식에서 한가지 이슈가 있는데 전통적인 자바의 동기화 키워드인 synchronized 를 사용하면 효과를 제대로 내기 어렵다는 것이다. 조금 더 정확히 얘기하면 synchronized 내에서 CPU 를 사용하는 CPU 바운드 작업을 할때는 상관없지만 synchronized 내에서 IO 가 발생하면 이때 이슈가 된다.

Thread.ofVirtual().start(() -> { httpCall() });

synchronized void httpCall() { 
  // http IO                               
}

생성된 virtual thread 는 httpCall() 메서드를 실행한다. httpCall() 메서드는 synchronized 키워드로 동기화되어 있으며 내부에서 http IO 를 발생시킨다. 만약 http 요청이 1초가 걸린다면 virtual thread 는 1초간 블록킹되며 이때 실제 OS 스레드인 플랫폼 스레드는 다른 virtual thread 에게 할당되어 코어 활용을 더욱 효율적으로 한다.

 

이게 virtual thread 의 의도지만 위 코드는 그렇게 동작하지 않는다. synchronized 키워드 내에서는 플랫폼 스레드가 pinning 되어 다른 virtual thread 에 할당되지 못하고, 이 때문에 virtual thread 의 이점을 살릴 수 없다.

 

이 문제를 해결하는 방법은 두가지인데 하나는 동기화 방법을 바꾸는 것이다.

private ReentrantLock lock = new ReentrantLock();

public void pinning() {                                 
    Thread.ofVirtual().start(() -> { httpCall(); });   
}                                                             
                                                       
void httpCall() {                                       
    lock.lock();                                       
    // http IO                                                   
    lock.unlock();                                     
}

synchronized 키워드 대신에 ReentrantLock 객체를 사용하는 것이다. ReentrantLock 은 java 1.5 에서 concurrent 패키지에 추가된 API 이다. 둘다 락을 잡는건 동일하지만 synchronized 를 이용하는 것과 ReentrantLock 을 이용할때 내부 메커니즘이 달라 ReentrantLock 을 이용하는 경우엔 플랫폼 스레드가 다른 virtual thread 에게 할당되지만 synchronized 를 이용하는 경우엔 위에서 언급한대로 다른쪽으로 할당되지 못한다.

 

두번째는 IO 작업을 락 내에서 하지 않는 것이다. 웬만한 경우엔 이 방식이 정답이다.

 

하지만 우리도 모르는 사이에 아주 핵심적인 곳에서 synchronized 동기화 내에서 IO 를 발생시키는 대표적인 부분이 있다. 바로 JDBC Driver 구현체들이다. 이때문에 virtual thread 가 세상에 나온 이후로 JDBC Driver 구현체들은 동기화 방식을 ReetrantLcok 으로 변경하는 작업을 진행했다. MySQL Connector/J 는 9.0.0 release notes 에 해당 변경이 포함됐다. virtual thread 를 이용한다면 버전을 확인해보자.

 

Synchronized blocks in the Connector/J code were replaced with ReentrantLocks. This allows carrier threads to unmount virtual threads when they are waiting on IO operations, making Connector/J virtual-thread friendly. Thanks to Bart De Neuter and Janick Reynders for contributing to this patch. (Bug #110512, Bug #35223851)

 

다만 이런 변경에 무색하게 그 이후 새로운 제안이 발표되었다. synchronized 키워드에서도 virtual thread 가 정상적으로 unmount 되도록 하는 내용의 JEP 491이다. 해당 내용은 JDK 24 부터 포함됐고, LTS 기준으로는 JDK 25 부터 포함되게 된다. JDK 24 이상을 사용하게 되면 미처 ReentrantLock 으로 변경하지 못한 라이브러리를 사용하더라도 virtual thread 의 이점을 살릴 수 있을 것이다. 

 

다만... 할 수 있었으면 처음부터 같이 해주지...

# 참고자료

https://openjdk.org/jeps/444

https://openjdk.org/jeps/491

https://dev.mysql.com/doc/relnotes/connector-j/en/news-9-0-0.html

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함