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 |
|---|---|---|
| 2021 | 2025. 11. 30. | Java 함수형 프로그래밍 - Lambda와 Method Reference |
| 2020 | 2025. 11. 30. | Lombok 활용 가이드 - 보일러플레이트 제거 |
| 2019 | 2025. 11. 30. | Java Record와 Sealed Class 활용 |
| 2018 | 2025. 11. 30. | 효과적인 Java 예외 처리 전략 |
| 2017 | 2025. 11. 30. | JVM 메모리 구조와 GC 튜닝 |
| 2016 | 2025. 11. 30. | Java 동시성 프로그래밍 - CompletableFuture |
| 2015 | 2025. 11. 30. | Stream API 고급 활용법 |
| 2014 | 2025. 11. 30. | Java 21 새 기능 - Virtual Threads와 Pattern Matching |
| 345 | 2016. 02. 07. | [ java ][ lambda ]1.8 람다식 관련 |
| 315 | 2015. 09. 14. | [ java ] 꼬리재귀 함수 ( Tail Recursion ) |