본문으로 바로가기
dobidugi
목록으로
CS

가상 스레드와 논블로킹 IO

8분
목차

둘 다 “I/O 대기 시간에 thread 자원을 낭비하지 말자” 라는 같은 문제를 다른 방식으로 해결한다.

기존 방식 (Platform Thread)

요청마다 OS thread 하나가 배정된다.

Request → [Thread1] → DB query → ⏳ blocking → 결과 받음 → Response

Non-blocking IO

ℹ️ INFO 핵심 Thread가 I/O를 기다리지 않는다. 서버 내부의 thread 관리 방식이지, 유저에게 response를 나중에 준다는 뜻이 아니다.

동작 방식

Request1 → [Thread1] → DB query (non-blocking) → Thread1 해방 → 다른 request 처리
                                                       ↓
                                             DB 결과 도착하면
                                                       ↓
                                        [Thread2 or 1] → callback 실행 → Response

Code style (Reactive)

return userRepository.findById(id)
    .flatMap(user -> orderRepository.findByUser(user)
        .map(order -> new Response(user, order)));

Non-blocking ≠ 응답을 나중에 주는 것

Non-blocking IO응답을 나중에 주는 것
Level서버 내부 thread 관리API design pattern
누가 결정framework/runtime개발자
목적thread 효율UX/비즈니스 요구사항

대부분의 REST API는 non-blocking IO를 쓰면서도 결과를 기다려서 synchronous하게 응답한다. “접수만 하고 나중에 결과 알림”은 비즈니스 설계의 선택이지 non-blocking이라서 그런 것이 아니다.

Virtual Thread (Project Loom)

ℹ️ INFO 핵심 동기식 코드를 쓰면서 non-blocking의 효율을 얻는다. JVM이 관리하는 lightweight thread.

동작 방식

Request1 → [VirtualThread1] → DB query (blocking code)
                                    ↓
                          JVM 감지: "I/O blocking이네"
                                    ↓
                          VirtualThread1을 carrier thread에서 분리 (unmount)
                                    ↓
                          Carrier thread → 다른 VirtualThread 처리
                                    ↓
                        DB 결과 도착 → VirtualThread1 다시 올림 (mount) → 계속 실행

JVM은 어떻게 blocking을 감지하나?

개발자가 뭔가를 요청하는 게 아니다. Java 21에서 JDK 표준 라이브러리의 blocking method 내부 구현이 바뀌었다.

Thread.sleep(), Socket.read(), InputStream.read() 등이 내부적으로:

  1. 현재 thread가 virtual thread인지 확인
  2. Virtual thread → carrier thread에서 unmount, I/O를 non-blocking으로 등록
  3. Platform thread → 기존처럼 그냥 blocking
// 개념적 흐름 (실제 JDK 내부)
public int read() {
    // 예전: OS-level blocking call → thread가 멈춤
    // 지금: virtual thread인가?
    //       → 맞으면 carrier thread에서 unmount
    //       → OS에 non-blocking으로 I/O 등록
    //       → 결과 오면 다시 mount
}

그래서 Thread.ofVirtual()로 실행하기만 하면 JDK가 알아서 처리한다. 코드를 바꿀 필요 없음.

⚠️ 주의 Pinning 주의 JDK 표준 라이브러리만 이렇게 대응된다. 다음 경우에는 JVM이 감지를 못해서 carrier thread가 묶임 (pinning):

  • JNI로 native code에서 blocking
  • synchronized block 안에서 I/O blocking

Code style (동기식 그대로)

User user = userRepository.findById(id);       // blocking → JVM이 알아서 unmount
Order order = orderRepository.findByUser(user); // 여기서도
return new Response(user, order);               // 깔끔

비교

Non-blocking IOVirtual Thread
문제Thread가 I/O에서 놀고있다Thread가 I/O에서 놀고있다
해결Thread를 안 기다리게 바꿈기다려도 괜찮게 바꿈
방법callback/reactive로 코드 변경JVM이 thread를 경량화
코드 복잡도높음낮음 (기존 코드 그대로)
제약없음synchronized, ThreadLocal 주의

언제 뭘 써야 하나?

Non-blocking IO (WebFlux)Virtual Thread
I/O-bound적합적합 (코드가 더 간단)
CPU-bound더 유리이점 없음
I/O + CPU 혼합상황에 따라 판단상황에 따라 판단

CPU-bound에서 Virtual Thread가 이점이 없는 이유

Virtual thread의 핵심 이점은 I/O blocking 시 carrier thread에서 unmount되는 것이다. 그런데 CPU 작업은 blocking이 발생하지 않으므로:

반면 non-blocking IO는 core 수만큼만 thread를 유지하므로 OS-level context switching이 최소화된다.

Virtual Thread는 context switching이 많이 발생하나?

Virtual thread가 수만 개여도 OS-level context switching은 거의 없다.

실제 OS context switching은 carrier thread 수(보통 CPU core 수)만큼만 발생한다. Virtual thread 간 전환은 JVM 내부의 lightweight한 작업이다.

정리

💡 TIP 결론 새 프로젝트에서 I/O-bound 위주라면 Virtual Thread가 훨씬 간단한 선택이다. CPU-bound가 많다면 **Non-blocking IO(Reactive)**를 고려하자.



이전 글
원티드 프론트엔드 프리온보딩 인턴쉽 회고록
다음 글
암복호화