개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Java › 스레드와 동시성

블로킹·논블로킹·가상 스레드

기다리는 동안 스레드는 무엇을 하나

요청마다 DB나 외부 API의 응답을 기다리는 시간이 있습니다. 그 시간에 스레드를 붙잡아 두는 방식(블로킹), 스레드를 놓아주고 응답이 오면 이어서 처리하는 방식(논블로킹), 그리고 블로킹처럼 코드를 쓰지만 기다릴 때 운영체제 스레드를 놓아주는 가상 스레드를 비교해 보세요.

실행으로 대조 · JDK 21에서 실제로 잰 값과 대조

이 실습에 나오는 개념

블로킹과 논블로킹
블로킹은 호출한 작업이 끝날 때까지 호출한 스레드가 멈춰서 다른 일을 하지 못하는 것이다. 논블로킹은 작업이 끝나지 않았어도 호출이 바로 반환되어 스레드가 다른 일을 할 수 있는 것이다. 일반적인 JDBC 호출은 DB 응답이 올 때까지 스레드가 멈추는 블로킹 호출이다.
동기와 비동기
동기는 작업을 요청한 쪽이 결과를 받을 때까지 기다린 뒤 다음으로 진행하는 방식이다. 비동기는 작업을 요청해 두고 결과를 기다리지 않고 다음 일을 진행하며, 결과는 나중에 콜백이나 Future 등으로 전달받는 방식이다.
가상 스레드(Virtual Thread)
JDK 21에서 정식 도입된 가벼운 스레드다. 기존 스레드는 운영체제 스레드와 일대일로 묶여 많이 만들 수 없지만, 가상 스레드는 JVM이 관리하며 소수의 운영체제 스레드 위에서 번갈아 실행된다. 대부분의 블로킹 호출에서 대기하는 동안에는 운영체제 스레드를 내주므로, 기존의 단순한 코드 방식으로도 많은 동시 요청을 처리할 수 있다. 다만 JDK 21~23에서는 synchronized 안에서 블로킹하면 운영체제 스레드를 붙잡았고(pinning), 이는 JDK 24에서 해소되었다.
Spring WebFlux와 리액티브
적은 수의 스레드로 많은 동시 요청을 처리하기 위한 스프링의 논블로킹 웹 프레임워크다. 요청당 스레드를 하나씩 쓰는 Spring MVC와 달리, I/O를 기다리는 동안 스레드를 붙잡지 않고 데이터가 준비되면 이어서 처리한다. 호출 경로 전체가 논블로킹이어야 효과가 있어 JPA 같은 블로킹 기술과는 맞지 않는다.
이벤트 루프
소수의 스레드가 여러 연결에서 발생하는 입출력 이벤트를 감지해 차례로 처리하는 구조다. 연결마다 스레드를 두지 않으므로 하나의 스레드가 많은 연결을 담당한다. 이 스레드에서 오래 걸리는 작업이나 블로킹 호출을 하면 그 스레드가 맡은 모든 연결의 처리가 멈춘다.
톰캣의 스레드 모델
톰캣은 요청 처리용 스레드를 풀로 관리하며, 요청 하나를 스레드 하나가 맡아 응답이 끝날 때까지 처리한다. 동시에 처리할 수 있는 요청 수는 최대 스레드 수(기본 200)로 제한되고, 모든 스레드가 사용 중이면 새 요청은 대기한다. 요청 처리 중 데이터베이스나 외부 호출을 기다리는 동안에도 스레드는 점유된 상태다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 0.0초: 요청 12건이 들어옵니다. 스레드 4개가 요청을 하나씩 맡습니다. 나머지 8건은 스레드가 빌 때까지 기다립니다.
  2. 0.1초: 각 스레드가 외부 응답을 기다립니다. 기다리는 동안 스레드는 아무 일도 하지 않지만 다른 요청을 맡을 수도 없습니다(블로킹).
  3. 2.0초: 응답이 도착해 요청 4건이 끝났습니다. 풀려난 스레드가 기다리던 요청을 맡습니다(남은 대기 4건).
  4. 4.0초: 응답이 도착해 요청 4건이 끝났습니다. 풀려난 스레드가 기다리던 요청을 맡습니다(남은 대기 0건).
  5. 6.0초: 응답이 도착해 요청 4건이 끝났습니다.

실행 결과

코드

// 스프링 MVC: 요청마다 스레드 하나가 끝까지 맡는다
// server.tomcat.threads.max=4
@GetMapping("/orders/{id}")
OrderDto get(@PathVariable long id) {
    Order order = orderRepository.findById(id);   // 응답이 올 때까지 스레드가 멈춰 기다림
    Delivery d = deliveryClient.track(order);     // (2초)
    return OrderDto.of(order, d);
}

이 모형의 전제

요청 처리에서 CPU를 쓰는 시간은 무시하고, 외부 응답을 기다리는 시간만 있다고 단순화했습니다. 이벤트 루프와 가상 스레드를 실어 나르는 OS 스레드는 2개로 고정했습니다(실제 기본값은 CPU 코어 수에 맞춰집니다). 가상 스레드는 JDK 21에서 정식 기능이 됐으며, JDK 21~23에서는 synchronized 블록 안에서 기다리면 OS 스레드를 내려놓지 못하는 제약이 있었고 JDK 24에서 해소됐습니다. 이 PC의 JDK 21에서 기다리는 시간을 0.2초로 줄여 실제로 쟀습니다. 요청 12건 기준으로 스레드 4개 풀은 619ms(세 번에 나눠 처리), 스레드 2개에서 블로킹하면 1,227ms, 가상 스레드는 OS 스레드 2개로 216ms, 스레드 2개에서 기다림을 등록만 하는 방식은 201ms였습니다. 모형의 비율(6초, 12초, 2초, 2초)과 같습니다. 스프링 WebFlux 자체를 띄운 것은 아니고 같은 원리를 JDK만으로 재현한 측정입니다.

앞 실습: 순차 호출과 병렬 호출 · 다음 실습: 스택과 힙

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요