Contents
see List작성일 2026. 07. 12.
Java API 동시 호출이 폭주할 때 생기는 실제 장애
외부 API를 여러 개 호출하는 서비스는 순간 요청 수가 커지면 곧바로 병목이 드러납니다. 가장 흔한 증상은 아래 순서입니다.
- 연결 타임아웃이 쌓이며 95퍼센타일 응답시간이 급등
- 쓰레드 풀(ExecutorService)에서 큐가 꽉 차고 신규 요청이 거부
- 한 개 실패가 여러 하위 호출을 연쇄 실패시키며 전체 요청이 실패
- 장애가 있어도 운영자가 사후에야 원인을 찾음
이 문서는 "Java에서 동시 호출 경로를 제어하고, 실패를 격리하며, 복구 가능하게 만드는" 기준을 바로 실무에 적용할 수 있도록 정리했습니다.
기본 전략: 동시성은 반드시 제한하고, timeout은 항상 분리한다
중요한 원칙은 세 가지입니다.
- 무한 병렬화 금지: 전체 요청에서 하위 API 동시 호출 개수에 상한을 둡니다.
- 동일 경로 timeout 통일: 연결/쓰기/읽기 시간 제한을 각각 분리합니다.
- 부분 실패 허용: 전체를 바로 실패 처리하지 않고, 실패 경로만 빠르게 완화해 실패 범위를 축소합니다.
가장 많이 쓰는 조합은 ExecutorService + Semaphore + HttpClient timeout + CompletableFuture 집계입니다.
실전 코드 예시
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class DownstreamBatch {
private static final ExecutorService apiPool =
Executors.newFixedThreadPool(32, r -> new Thread(r, "api-worker-" + System.nanoTime()));
private static final Semaphore inFlight = new Semaphore(24);
private static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1))
.build();
public static Map callAll(List endpoints) {
List> tasks = endpoints.stream().map(url ->
CompletableFuture.supplyAsync(() -> {
boolean acquired = false;
try {
acquired = inFlight.tryAcquire(1, Duration.ofMillis(200));
if (!acquired) return "SKIP_IN_FLIGHT_FULL";
HttpRequest req = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(2))
.GET()
.build();
return http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(r -> r.body())
.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
return "TIMEOUT:" + url;
} catch (Exception e) {
return "ERROR:" + e.getClass().getSimpleName();
} finally {
if (acquired) inFlight.release();
}
}, apiPool)
).collect(Collectors.toList());
Map results = new java.util.LinkedHashMap<>();
for (CompletableFuture f : tasks) {
try { results.put("ok", f.get(3, TimeUnit.SECONDS)); }
catch (Exception e) { results.put("ok", "FALLBACK"); }
}
return results;
}
}
운영 체크포인트
위 코드를 그대로 붙여 쓰는 것보다 운영 정책을 함께 맞춰야 효과가 납니다.
- 동일 API 그룹별로 최대 동시 요청 수를 분리합니다.
- 각 그룹 timeout을 SLO 기준으로 정합니다. 1초 타임아웃만 쓰지 말고
connect/read/write를 분리하세요. - 예외를 숨기지 말고 메트릭으로 분류합니다.
TIMEOUT,ERROR,fallback를 따로 집계해야 원인 분리가 쉽습니다. - 재시도는 무조건 지수 백오프를 사용하고 동일 시간대에 동시에 재시도하지 않도록 jitter를 넣습니다.
기술 요약 체크리스트
- 요청마다 최소 1개 이상의 동시성 상한이 있는가?
- 전체 실패 시나리오를 부분 실패로 축소할 fallback이 있는가?
- timeout이 연결/요청/응답 단계로 분리돼 있는가?
- in-flight, queue length, timeout 비율, 실패 유형별 집계가 모니터링되는가?
- 장애 재현을 위해 부하 테스트에서 동일 패턴을 자동 검증하고 있는가?
java
| No | 작성일 | Title |
|---|---|---|
| 2491 | 2026. 04. 14. | Java 21/25 Virtual Threads 완벽 가이드: 기존 스레드 모델과의 차이, 실전 활용법 |
| 2470 | 2026. 04. 13. | Java 25 Virtual Threads & Structured Concurrency 완벽 가이드 |
| 2450 | 2026. 04. 12. | Java 21 Virtual Threads 완벽 가이드: Project Loom으로 수백만 스레드 다루기 |
| 2426 | 2026. 04. 11. | Java 24 Primitive Type Pattern Matching 완벽 가이드 - switch와 instanceof 확장 |
| 2390 | 2026. 04. 09. | Java 26 신기능 완벽 가이드 - AOT 캐시, Compact Headers, 패턴 매칭 실전 활용 |
| 2369 | 2026. 04. 08. | Java 21/25 완벽 가이드: Virtual Threads, Record Patterns, Structured Concurrency 실전 적용 |
| 2351 | 2026. 04. 07. | Java 23 Gatherers API 실전 가이드 - Stream 중간 연산을 직접 정의하는 방법 |
| 2337 | 2026. 04. 06. | Java 24 Structured Concurrency 실전 가이드 - StructuredTaskScope로 안전한 동시성 구현 |
| 2322 | 2026. 04. 05. | Java 24 Compact Object Headers로 힙 메모리 25% 절약하기 |
| 2298 | 2026. 04. 05. | Java 24 Stream Gatherers 실전 활용법: 커스텀 중간 연산 만들기 |