|

Java virtual threads pinning이란 무엇인가: 가벼운 스레드도 막힐 수 있는 순간

Java virtual threads pinning 대표 이미지
가벼운 스레드도 carrier thread를 붙잡으면 기대한 만큼 늘어나지 않는다

Java virtual threads pinning은 가상 스레드가 가볍다는 말을 들은 뒤 꼭 한 번은 짚고 넘어가야 하는 주제입니다.

virtual threads는 blocking 코드를 훨씬 단순하게 다루게 해주지만, 모든 blocking 상황에서 비용이 사라지는 것은 아닙니다.


Java virtual threads pinning을 먼저 한 줄로 이해하기

Java virtual threads pinning 핵심 카드
pinning을 볼 때 확인할 실무 기준

virtual thread는 실제 OS thread 위에서 실행됩니다. 이때 virtual thread를 실행하는 실제 스레드를 보통 carrier thread라고 부릅니다.

일반적인 blocking 상황에서는 virtual thread가 carrier thread에서 내려와 다른 virtual thread에게 실행 기회를 줄 수 있습니다. 하지만 어떤 상황에서는 내려오지 못하고 carrier thread를 붙잡습니다. 이것이 pinning입니다.

pinning은 virtual thread가 가벼운 작업 단위라는 장점을 잠시 잃는 순간으로 이해하면 됩니다.


virtual threads가 해결하려는 문제

기존 서버 코드에서는 요청마다 platform thread를 하나씩 붙이면 스레드 수와 메모리 비용이 빨리 커졌습니다. 그래서 스레드 풀, 비동기 콜백, reactive 코드가 많이 쓰였습니다.

JEP 444의 virtual threads는 blocking 스타일 코드를 유지하면서도 훨씬 많은 동시 작업을 다루기 쉽게 만드는 것이 목표입니다.

즉 virtual threads의 핵심 가치는 “blocking 코드를 더 읽기 쉽게 유지하면서 동시성을 늘리는 것”입니다. 성능 마법이라고 보면 안 됩니다.


pinning은 언제 문제가 될까

JDK 21 문서 기준으로 virtual thread가 특정 작업 중 carrier thread에서 unmount되지 못하는 경우가 있습니다. 대표적으로 monitor를 잡은 상태, 즉 synchronized 블록 안에서 blocking 작업을 오래 하는 경우가 주의 대상입니다.

class ReportService {
    private final Object lock = new Object();

    String loadReport(String id) throws IOException {
        synchronized (lock) {
            // 잠금을 잡은 상태에서 오래 걸리는 I/O를 하면 pinning 위험이 커진다.
            return httpClient.fetch("/reports/" + id);
        }
    }
}

이 코드는 동작 자체가 틀렸다고 단정할 수는 없습니다. 하지만 lock을 잡은 채 네트워크 I/O를 기다리면 virtual thread가 carrier thread를 오래 붙잡을 수 있습니다.


lock 범위와 blocking 작업을 분리하기

해결 방향은 단순합니다. 오래 걸리는 작업과 짧은 임계 구역을 분리하는 것입니다. 공유 상태를 읽거나 갱신하는 부분만 짧게 잠그고, 네트워크 호출은 lock 밖에서 실행합니다.

class ReportService {
    private final Object lock = new Object();
    private final Map<String, String> cache = new HashMap<>();

    String loadReport(String id) throws IOException {
        synchronized (lock) {
            String cached = cache.get(id);
            if (cached != null) return cached;
        }

        String fetched = httpClient.fetch("/reports/" + id);

        synchronized (lock) {
            cache.put(id, fetched);
        }
        return fetched;
    }
}

핵심은 synchronized 자체를 무조건 금지하는 것이 아닙니다. lock을 잡은 동안 오래 기다리는 작업을 하지 않는 것입니다.


virtual threads를 쓸 때의 현실적인 점검 기준

  • 요청 처리 코드가 blocking 스타일이어도 읽기 쉬운가
  • synchronized 안에서 I/O, sleep, 긴 계산을 하지 않는가
  • thread-local 사용량이 과도하지 않은가
  • DB 커넥션 풀, 외부 API 제한처럼 실제 병목이 따로 있지 않은가
  • 스레드 덤프나 JFR로 virtual thread 상태를 확인할 수 있는가

virtual threads를 도입해도 DB 커넥션 풀 크기나 외부 API rate limit이 사라지지는 않습니다. 동시 요청이 늘어나는 만큼 하위 자원 제한은 더 빨리 드러날 수 있습니다.


정리

출처 확인: OpenJDK JEP 444Oracle virtual threads 문서를 기준으로 pinning 설명 범위를 잡았습니다.

Java virtual threads pinning은 가상 스레드를 쓰면 모든 blocking 비용이 사라진다는 오해를 막아주는 개념입니다.

virtual threads는 코드를 단순하게 만들 수 있지만, lock 범위, blocking 작업, 하위 자원 제한을 함께 봐야 합니다. 이전 글인 Java virtual threads란 무엇인가를 먼저 읽었다면, 이번 글은 실무 주의점으로 이어지는 후속 기준이 됩니다.

함께보면 좋은 글