Contents
see List여러 외부 API를 순차 호출하면 왜 문제가 될까
주문 상세 화면이나 관리자 대시보드에서는 상품, 재고, 회원 등 여러 서비스를 함께 조회하는 일이 많습니다. 각 API가 300ms씩 걸린다면 세 요청을 순서대로 실행했을 때 화면은 최소 900ms를 기다립니다. 독립적인 조회 작업은 병렬화할 수 있지만, 무작정 스레드를 늘리면 연결 수가 급증하고 장애가 난 한 서비스가 전체 요청을 오래 붙잡을 수 있습니다. Java의 CompletableFuture는 비동기 작업의 조합, 타임아웃, 오류 대체를 코드에 명시할 수 있어 이런 흐름을 관리하기 좋습니다.
핵심은 세 가지입니다. 요청과 무관한 작업만 병렬로 시작하고, 실행용 Executor를 공용 풀에 맡기지 않으며, 각 외부 의존성의 실패를 전체 실패와 구분해야 합니다. 특히 웹 애플리케이션에서 기본 ForkJoinPool.commonPool을 I/O 작업에 계속 사용하면 다른 비동기 작업과 자원을 경쟁할 수 있으므로 별도 풀을 두는 편이 안전합니다.
먼저 작업 성격과 동시성 한도를 정한다
외부 HTTP 호출은 CPU 계산보다 대기 시간이 긴 I/O 작업입니다. 풀 크기는 단순히 CPU 코어 수로 정하지 말고, 동시 사용자 수, 대상 API의 허용 연결 수, 애플리케이션의 커넥션 풀을 함께 고려해야 합니다. 예를 들어 한 화면에서 세 API를 호출하고 피크 시 30개의 요청을 동시에 처리한다면 잠재 호출 수는 90개입니다. 대상 서비스가 작은 연결 한도만 제공한다면 호출 풀을 제한하고, 초과 요청은 빠르게 거절하거나 대기열을 짧게 유지해야 합니다.
ExecutorService apiExecutor = new ThreadPoolExecutor(8, 16, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.CallerRunsPolicy());위 예시는 코어 8개, 최대 16개 스레드와 100개 대기열을 둡니다. 값 자체가 정답은 아닙니다. 운영 환경에서 평균 지연 시간, 활성 스레드, 대기열 길이, 거절 횟수를 측정해 조정해야 합니다. CallerRunsPolicy는 포화 시 호출 스레드가 작업을 처리해 무한 적재를 줄일 수 있으나 웹 요청 지연을 늘릴 수 있습니다. 사용자 응답을 우선해야 하는 서비스라면 명시적 거절과 429 또는 503 응답이 더 적합한 경우도 있습니다.
독립 조회를 병렬로 조합하는 기본 패턴
supplyAsync로 각 조회를 같은 Executor에서 시작하고 allOf로 완료 시점을 모읍니다. join은 예외를 CompletionException으로 감싸므로 결과를 모으는 위치에서 원인을 분류할 수 있게 설계합니다. 작업 내부에서 join을 중첩해 블로킹하지 말고, 모든 의존 작업을 먼저 만든 뒤 조합하는 것이 중요합니다.
CompletableFuture<Product> productFuture = CompletableFuture.supplyAsync(() -> productClient.find(productId), apiExecutor).orTimeout(800, TimeUnit.MILLISECONDS); CompletableFuture<Stock> stockFuture = CompletableFuture.supplyAsync(() -> stockClient.find(productId), apiExecutor).completeOnTimeout(Stock.unknown(), 500, TimeUnit.MILLISECONDS); CompletableFuture<MemberGrade> gradeFuture = CompletableFuture.supplyAsync(() -> memberClient.grade(memberId), apiExecutor).exceptionally(ex -> MemberGrade.GUEST); ProductView view = CompletableFuture.allOf(productFuture, stockFuture, gradeFuture).thenApply(ignored -> new ProductView(productFuture.join(), stockFuture.join(), gradeFuture.join())).join();상품 정보는 화면의 핵심 데이터이므로 800ms 안에 얻지 못하면 실패시키고, 재고는 일시적으로 알 수 없음 상태를 표시하며, 회원 등급은 기본값으로 대체했습니다. 모든 API를 같은 방식으로 감추면 장애 사실을 놓치거나 잘못된 업무 처리가 발생할 수 있습니다. 데이터마다 필수 여부와 대체 가능 여부를 먼저 정의해야 합니다.
타임아웃과 취소를 오해하지 않는다
orTimeout은 CompletableFuture의 완료를 시간 제한하지만, 이미 시작된 동기 HTTP 호출을 자동으로 중단하지는 않을 수 있습니다. 실제 네트워크 자원을 보호하려면 사용하는 HTTP 클라이언트에도 연결과 응답 타임아웃을 설정해야 합니다. Java HttpClient를 사용한다면 요청 단위 timeout을 같이 지정할 수 있습니다.
HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofMillis(700)).GET().build(); CompletableFuture<HttpResponse<String>> response = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());응답 코드 5xx, JSON 파싱 오류, 연결 실패, 시간 초과는 서로 다른 원인입니다. exception handling에서 모두 기본값으로 바꾸기보다 원인을 구조화해 로그와 메트릭으로 남겨야 합니다. 요청 ID, 대상 서비스명, 결과 코드, 경과 시간은 운영 분석에 특히 유용합니다. 단, 인증 헤더나 본문 전체처럼 민감할 수 있는 값은 로그에 남기지 않습니다.
재시도는 제한적으로 적용한다
재시도는 일시적 네트워크 오류에는 도움이 되지만, 이미 과부하인 서비스를 더 악화시킬 수 있습니다. GET처럼 멱등인 조회에만 짧은 횟수와 지수 백오프를 적용하고, 주문 생성처럼 중복 처리 위험이 있는 요청은 멱등성 키나 서버 측 중복 방지 없이는 자동 재시도하지 않아야 합니다. 장애 기간에는 서킷 브레이커로 빠르게 실패하고 캐시 또는 안내 응답으로 전환하는 정책도 필요합니다.
배포 전 체크리스트
- 병렬화한 호출이 실제로 서로 독립적인지 확인합니다.
- 외부 API별 HTTP 타임아웃과 CompletableFuture 타임아웃을 모두 설정합니다.
- 전용 Executor의 스레드, 대기열, 포화 정책을 부하 기준으로 정합니다.
- 필수 데이터와 대체 가능한 데이터를 구분해 오류 처리 규칙을 문서화합니다.
- 지연 시간, 실패율, 대기열, 타임아웃 횟수를 관측하고 알림 기준을 둡니다.
- 변경과 결제 요청에는 멱등성 보장 없이 자동 재시도를 적용하지 않습니다.
CompletableFuture는 단순히 응답 시간을 줄이는 도구가 아닙니다. 의존 서비스의 지연과 실패를 어디까지 허용할지 코드로 드러내는 방법입니다. 전용 실행 자원, 명확한 시간 제한, 데이터별 대체 정책을 함께 갖추면 병렬 호출이 많은 Java API도 예측 가능한 방식으로 운영할 수 있습니다.
java
| No | 작성일 | Title |
|---|---|---|
| 2134 | 2026. 02. 11. | Record와 Sealed Class로 견고한 도메인 모델링 |
| 2133 | 2026. 02. 11. | Virtual Thread 실전 활용: 동시성 프로그래밍의 혁명 |
| 2132 | 2026. 02. 11. | Java 23 새로운 기능 완전 정리: Pattern Matching의 진화 |
| 2023 | 2025. 11. 30. | Java 직렬화와 JSON 처리 - Jackson 활용 |
| 2022 | 2025. 11. 30. | Java 테스트 - JUnit 5와 Mockito |
| 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 튜닝 |