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 |
|---|---|---|
| 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 종료를 제대로 연결하기 |
| 2985 | 2026. 06. 09. | Java 컨테이너 메모리 운영 가이드: 힙, 직접 메모리, GC 로그를 함께 점검하기 |
| 2928 | 2026. 06. 01. | Java Virtual Threads 운영 전환 가이드: 블로킹 API, 커넥션 풀, 관측 지표 점검법 |
| 2874 | 2026. 05. 24. | Java 서비스 ThreadPoolExecutor 포화 방지: 큐 크기, 거절 정책, 백프레셔 실전 가이드 |
| 2799 | 2026. 05. 16. | Java 운영 장애 분석을 위한 JFR·jcmd 실전 가이드: 재시작 없이 CPU·메모리 증거 남기기 |
| 2603 | 2026. 04. 24. | Java 26 Pattern Matching 실전 가이드: Records · Sealed Classes · Primitive Patterns로 분기 없는 도메인 모델링 |
| 2518 | 2026. 04. 16. | Java 26 Virtual Threads & Structured Concurrency 실전 가이드: 가상 스레드로 고성능 서버 구축하기 |