Contents
see List가상 스레드는 요청 제한을 없애 주지 않는다
Java 가상 스레드(virtual thread)는 대기 시간이 많은 작업을 적은 수의 운영체제 스레드로 처리할 수 있게 해 준다. HTTP 호출, JDBC 조회, 파일 I/O처럼 결과를 기다리는 시간이 긴 서버에서 특히 유용하다. 하지만 가상 스레드를 도입했다고 해서 외부 API, 데이터베이스, 연결 풀의 처리 한계가 사라지는 것은 아니다. 요청마다 가상 스레드를 만들면 애플리케이션 내부 큐는 줄어들 수 있지만, 느린 외부 시스템으로 동시에 너무 많은 요청이 몰려 타임아웃과 재시도가 연쇄될 수 있다.
운영에서 중요한 기준은 스레드 수가 아니라 각 의존성의 동시 처리 가능 수다. 예를 들어 결제 API가 동시 20건을 안정적으로 처리하고 DB 커넥션 풀이 30개라면, 웹 요청이 수천 건 들어와도 해당 자원으로 나가는 동시 작업은 별도로 제한해야 한다. 가상 스레드는 대기 중인 요청을 저렴하게 보관하는 수단이고, 세마포어·타임아웃·큐 제한은 과부하를 막는 수단이다.
먼저 작업 유형을 나눈다
가상 스레드는 블로킹 I/O 작업에 적합하다. 반대로 이미지 변환, 대용량 암호화, 복잡한 JSON 변환처럼 CPU를 오래 점유하는 작업을 무제한 가상 스레드로 실행하면 CPU 경쟁만 늘어난다. CPU 작업은 코어 수에 맞춘 제한된 Executor로 분리하고, 외부 연동 작업은 대상 시스템별 동시성 제한을 둔다.
- 웹 요청 처리: 요청 단위 가상 스레드를 사용해 대기 효율을 높인다.
- 외부 HTTP·DB 호출: 대상별 세마포어와 명확한 연결·응답 타임아웃을 적용한다.
- CPU 집약 작업: 고정 크기 플랫폼 스레드 풀 또는 작업 큐로 분리한다.
- 재시도 작업: 즉시 반복하지 말고 최대 횟수와 지수 백오프를 둔다.
세마포어로 외부 API 동시성 제한하기
아래 예제는 가상 스레드 Executor에서 외부 배송 API 호출을 수행하되, 실제 동시 호출은 20개까지만 허용한다. 획득 대기 시간도 제한해야 요청이 무한정 쌓이지 않는다. 제한 시간 안에 자리를 얻지 못하면 빠르게 실패시키거나, 서비스 정책에 따라 재시도 큐로 넘긴다.
import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; final class DeliveryClient { private final Semaphore permits = new Semaphore(20); String track(String invoice) throws Exception { if (!permits.tryAcquire(300, TimeUnit.MILLISECONDS)) { throw new IllegalStateException("배송 API가 혼잡합니다"); } try { return callExternalApi(invoice); } finally { permits.release(); } } }실제 호출 메서드에는 연결 타임아웃과 응답 타임아웃을 모두 설정한다. 세마포어 값 20은 고정된 정답이 아니다. 외부 서비스의 계약상 제한, 평균·최대 응답 시간, 오류율, 애플리케이션 인스턴스 수를 함께 고려한다. 인스턴스가 4대라면 각 인스턴스에 20을 설정했을 때 전체 동시 호출은 최대 80이 된다. 공급자가 전역 40건을 권장한다면 인스턴스별 제한을 낮추거나, Redis 같은 공유 제한 장치를 검토해야 한다.
요청 범위 안에서 취소를 전파한다
하나의 화면을 만들기 위해 주문, 재고, 회원 정보를 동시에 조회하는 경우에는 하위 작업의 생명주기를 요청 범위에 묶는 것이 중요하다. 한 작업이 실패했는데 다른 작업이 계속 실행되면 자원 낭비와 불필요한 외부 호출이 발생한다. Java의 StructuredTaskScope 계열 API는 이런 병렬 작업을 하나의 범위로 관리하는 데 도움이 되지만, 사용 중인 JDK에서의 제공 상태와 호환성을 먼저 확인해야 한다. 어떤 방식이든 성공 조건, 취소 조건, 전체 제한 시간을 코드에 명시해야 한다.
단순한 Executor 사용 시에도 Future를 오래 방치하지 말고, 요청 종료·타임아웃 시 취소를 전파해야 한다. HTTP 클라이언트, JDBC 드라이버, 메시지 클라이언트가 인터럽트와 취소를 실제로 처리하는지도 통합 테스트에서 확인한다. 가상 스레드는 블로킹을 감춰 주므로, 취소가 되지 않는 호출은 누적될 때 더 늦게 발견될 수 있다.
연결 풀과 재시도 정책을 분리해 본다
DB 커넥션 풀의 최대 수보다 많은 작업이 동시에 DB 접근을 시도하면, 가상 스레드가 많아도 결국 풀 대기열에서 기다린다. 이때 요청 타임아웃보다 커넥션 획득 타임아웃이 길면 사용자는 늦게 실패하고 서버는 더 많은 대기 작업을 갖게 된다. 커넥션 획득 시간, 쿼리 실행 시간, HTTP 응답 시간을 각각 측정하고 상위 요청의 마감 시간보다 짧게 설정한다.
재시도는 일시적인 네트워크 오류에만 제한적으로 적용한다. 인증 오류, 잘못된 요청, 제한 초과처럼 재시도로 해결되지 않는 응답은 즉시 실패시킨다. 재시도가 필요한 경우에도 지수 백오프와 난수를 더하고, 재시도 자체가 세마포어 허가를 점유하도록 설계해야 한다. 그렇지 않으면 장애 발생 시 재시도 폭주가 정상 요청을 밀어내는 상황이 생긴다.
관측 지표를 함께 준비한다
도입 전후 비교를 위해 요청 처리 시간만 보지 말고 대기열과 의존성 지표를 수집한다. 외부 API별 세마포어 사용 중인 허가 수, 허가 획득 실패 수, 타임아웃 수, 재시도 수를 메트릭으로 기록한다. DB는 활성 커넥션, 커넥션 대기 시간, 느린 쿼리를 별도로 본다. 가상 스레드 수가 증가하는데 완료 요청 수가 늘지 않는다면 병목은 대개 외부 I/O, 커넥션 풀, CPU 중 하나다.
- 외부 시스템별 동시 호출 상한과 인스턴스 전체 합계를 계산했는가
- 연결·응답·전체 요청 타임아웃이 각각 필요한 곳에 설정됐는가
- 과부하 시 빠른 실패, 재시도, 사용자 응답 정책이 정해졌는가
- CPU 작업과 블로킹 I/O 작업이 분리됐는가
- 동시성 제한 거부와 타임아웃을 대시보드·알림으로 확인할 수 있는가
가상 스레드의 효과는 더 많은 요청을 무제한 실행하는 데 있지 않다. 제한된 의존성 자원을 보호하면서 대기 중인 작업을 예측 가능하게 다루는 데 있다. 대상별 한도, 취소, 타임아웃, 관측 지표를 함께 갖추면 점진적인 전환과 장애 원인 분석이 훨씬 쉬워진다.
java
| No | 작성일 | Title |
|---|---|---|
| 3420 | 2026. 08. 30. | Java 가상 스레드 도입 전 확인할 것: 동시성 제한과 외부 API 백프레셔 설계 |
| 3388 | 2026. 08. 22. | Java CompletableFuture로 외부 API 병렬 호출을 안전하게 구성하는 방법 |
| 3356 | 2026. 08. 14. | Java record와 sealed interface로 안전한 API 응답 모델 만들기 |
| 3324 | 2026. 08. 05. | Java 가상 스레드 실전 적용 가이드: I/O 중심 API의 동시성 구조 바꾸기 |
| 3292 | 2026. 07. 28. | Java 메모리 사용량이 계속 늘어날 때: Heap Dump와 JFR로 누수 원인 찾기 |
| 3261 | 2026. 07. 20. | Java record로 API DTO를 단순하게 만드는 방법: 검증·변환·직렬화 실전 적용 |
| 3235 | 2026. 07. 12. | Java API 동시 호출 폭주 대응: Thread Pool, timeout, 장애 격리로 안정성 올리기 |
| 3178 | 2026. 07. 03. | Java 메모리 누수 진단 가이드: jcmd와 JFR로 원인 좁히기 |
| 3130 | 2026. 06. 25. | Java 가상 스레드 적용 전 점검: 블로킹 I/O와 동시성 제한 운영 가이드 |
| 3074 | 2026. 06. 17. | Java 스레드 작업 취소 설계: interrupt, Future, ExecutorService 종료를 제대로 연결하기 |