외부 API 호출 시 간헐적 실패 원인 분석: HTTP API 장애를 추적하는 실무 기준

외부 HTTP API 호출은 평소에는 잘 동작하다가도 특정 시간대나 일부 요청에서만 실패하는 경우가 있습니다. 이런 문제는 단순히 “상대 서버가 불안정하다”로 넘기기보다, 네트워크, 타임아웃, 커넥션, 재시도, 응답 처리 흐름을 차분히 나눠서 봐야 원인을 좁힐 수 있습니다.

백엔드 시스템에서 외부 API를 호출하는 코드는 흔합니다. 결제 승인, 본인 인증, 메시지 발송, 배송 조회, 지도 검색, 번역, 알림 발송처럼 서비스의 일부 기능이 외부 HTTP API에 의존하는 경우가 많습니다.

문제는 실패가 항상 명확하게 재현되지 않는다는 점입니다. 같은 요청을 다시 보내면 성공하고, 로컬에서는 잘 되는데 운영 환경에서만 실패하며, 특정 시간대에만 오류가 늘어나는 식입니다. 이런 간헐적 실패는 로그 한 줄만 보고 판단하면 원인을 잘못 짚기 쉽습니다.

 

HTTP API 간헐적 실패는 왜 분석하기 어려운가

HTTP API 호출 실패는 단순히 성공과 실패로만 나뉘지 않습니다. 요청이 상대 서버까지 도달하지 못했을 수도 있고, 도달했지만 응답을 받기 전에 타임아웃이 났을 수도 있습니다. 또는 응답은 받았지만 애플리케이션이 파싱하지 못했을 수도 있습니다.

실무에서는 이 차이를 구분하지 못해서 분석이 길어지는 경우가 많습니다. 예를 들어 Java 애플리케이션에서 Read timed out이 발생했다고 해서 무조건 상대 API 서버가 죽었다고 볼 수는 없습니다. 응답이 늦은 것인지, 중간 네트워크 구간에서 지연된 것인지, 클라이언트의 타임아웃 설정이 너무 짧은 것인지 따로 봐야 합니다.

간헐적 실패는 특히 “일부 요청만 실패한다”는 특징 때문에 더 복잡합니다. 전체 API가 완전히 중단된 상황이 아니기 때문에 모니터링 대시보드에서는 정상처럼 보일 수 있습니다. 하지만 사용자 입장에서는 결제 실패, 인증 실패, 알림 미발송처럼 명확한 서비스 오류로 느껴집니다.

 

외부 HTTP API 실패를 먼저 분류하는 방법

HTTP API 호출 실패를 분석할 때는 먼저 실패 지점을 분류하는 편이 좋습니다. 원인 후보를 한꺼번에 보는 것보다, 요청 전송 전, 연결 단계, 응답 대기 단계, 응답 처리 단계로 나누면 로그를 해석하기 쉬워집니다.

1. 연결 단계에서 실패한 경우

연결 단계 실패는 클라이언트가 외부 API 서버와 TCP 연결을 맺는 과정에서 문제가 생긴 경우입니다. 대표적으로 DNS 조회 실패, connection timeout, connection refused, TLS handshake 실패가 있습니다.

java.net.ConnectException: Connection refused
java.net.SocketTimeoutException: connect timed out
javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake

이 단계의 오류는 요청이 상대 API 애플리케이션까지 도달하지 않았을 가능성이 높습니다. 방화벽, 보안 그룹, 프록시, DNS, 인증서, 네트워크 경로를 함께 확인해야 합니다. 상대 서버의 비즈니스 로직 문제라고 단정하면 분석 방향이 틀어질 수 있습니다.

2. 응답 대기 중 실패한 경우

연결은 성공했지만 응답을 기다리는 중에 실패하는 경우도 많습니다. 흔히 Read timed out, SocketTimeoutException, connection reset 같은 형태로 나타납니다.

java.net.SocketTimeoutException: Read timed out
java.net.SocketException: Connection reset
java.io.EOFException: Unexpected end of stream

이 경우에는 요청이 상대방에게 전달되었을 가능성이 있습니다. 따라서 재시도를 무조건 적용하면 중복 처리 문제가 생길 수 있습니다. 결제 승인, 포인트 차감, 쿠폰 발급처럼 상태를 변경하는 API라면 특히 조심해야 합니다.

3. 응답은 왔지만 처리에 실패한 경우

HTTP 상태 코드는 200으로 왔지만 응답 본문 구조가 예상과 다르거나, JSON 파싱에 실패하거나, 문자 인코딩이 맞지 않아 실패할 수도 있습니다. 이런 오류는 네트워크 장애처럼 보이지만 실제로는 애플리케이션 레벨의 계약 불일치인 경우가 많습니다.

com.fasterxml.jackson.databind.exc.MismatchedInputException
org.springframework.web.client.RestClientException
org.springframework.http.converter.HttpMessageNotReadableException

외부 API는 문서와 다른 응답을 내려주는 경우가 있습니다. 성공 응답과 실패 응답의 스키마가 다르거나, 특정 오류 상황에서 HTML 에러 페이지가 내려오는 경우도 있습니다. 이럴 때는 상태 코드, 응답 헤더, 응답 본문 일부를 함께 남겨야 분석이 가능합니다.

 

외부 API 호출 실패에서 자주 보는 원인

외부 API 호출 실패 원인은 하나로 고정되지 않습니다. 다만 실무에서 자주 확인하는 지점은 어느 정도 정해져 있습니다. 아래 항목을 순서대로 보면 막연한 추측을 줄일 수 있습니다.

타임아웃 설정이 없거나 너무 긴 경우

외부 HTTP API 호출 코드에서 타임아웃을 명시하지 않으면 문제가 커질 수 있습니다. 요청이 응답을 오래 기다리면서 애플리케이션의 작업 스레드를 점유하고, 이후 요청 처리에도 영향을 줄 수 있습니다.

반대로 타임아웃이 너무 짧아도 문제가 됩니다. 상대 API의 정상 응답 시간이 상황에 따라 조금씩 흔들리는데, 클라이언트가 지나치게 짧은 기준으로 끊어버리면 간헐적 실패가 늘어납니다. 타임아웃은 “짧게 잡으면 안전하다”가 아니라, 기능의 성격과 상대 API의 응답 특성을 보고 정해야 합니다.

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(3))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/orders"))
    .timeout(Duration.ofSeconds(5))
    .GET()
    .build();

연결 타임아웃과 응답 타임아웃은 구분해서 보는 편이 좋습니다. 연결 자체가 늦은 문제인지, 연결 후 응답 생성이 늦은 문제인지에 따라 확인해야 할 대상이 달라집니다.

커넥션 풀 고갈 또는 재사용 문제

Spring Boot에서 외부 API를 자주 호출한다면 HTTP 클라이언트의 커넥션 관리도 확인해야 합니다. 매 요청마다 새 연결을 만들면 비효율적이고, 반대로 커넥션 풀 설정이 부족하면 대기 시간이 길어질 수 있습니다.

간헐적으로 connection reset이 보이는 경우에는 오래된 커넥션을 재사용하다가 상대 서버나 중간 장비가 이미 끊은 연결을 다시 쓰는 상황도 의심할 수 있습니다. 이 문제는 로그만 보면 상대 API가 갑자기 연결을 끊은 것처럼 보이지만, 실제로는 클라이언트의 커넥션 재사용 정책과 맞물려 발생할 수 있습니다.

PoolingHttpClientConnectionManager connectionManager =
    new PoolingHttpClientConnectionManager();

connectionManager.setMaxTotal(100);
connectionManager.setDefaultMaxPerRoute(20);

풀 크기는 무조건 크게 잡는다고 좋은 것은 아닙니다. 호출 빈도, 외부 API별 동시 호출 수, 요청 처리 시간, 서버 스레드 수를 함께 봐야 합니다. 팀에서 여러 외부 API를 같은 HTTP 클라이언트로 호출한다면 API별 분리 여부도 검토할 만합니다.

상대 API의 제한 정책

외부 API는 대부분 호출 제한 정책을 가지고 있습니다. 초당 요청 수, 분당 요청 수, 일일 호출량, 토큰별 제한, IP별 제한처럼 기준이 다양합니다. 이 제한을 넘으면 429, 403, 503 같은 상태 코드가 내려올 수 있습니다.

여기서 중요한 점은 실패가 항상 같은 상태 코드로 오지 않는다는 것입니다. 어떤 API는 명확히 429를 반환하지만, 어떤 API는 일반 오류 코드와 메시지만 내려줍니다. 따라서 외부 API 문서의 에러 응답 규칙을 코드와 운영 로그 양쪽에 반영해야 합니다.

HTTP/1.1 429 Too Many Requests
Retry-After: 10

{
  "code": "RATE_LIMIT_EXCEEDED",
  "message": "Too many requests"
}

호출 제한이 있는 API라면 재시도만으로 해결하려 하면 안 됩니다. 요청량을 조절하거나, 큐를 두거나, 캐시 가능한 응답은 캐시하는 방식까지 함께 고려해야 합니다.

프록시, 로드밸런서, NAT 구간 문제

외부 API 호출은 애플리케이션 서버에서 상대 서버로 바로 가는 것처럼 보이지만, 실제로는 여러 네트워크 구간을 거칩니다. 사내 프록시, 클라우드 NAT Gateway, 방화벽, 로드밸런서, WAF, 보안 장비가 중간에 있을 수 있습니다.

운영 환경에서만 실패하고 로컬에서는 재현되지 않는다면 이 구간을 확인해야 합니다. 특히 특정 서버 인스턴스에서만 실패하거나, 특정 AZ 또는 특정 네트워크 대역에서만 실패한다면 애플리케이션 코드보다 네트워크 경로가 더 중요한 단서가 됩니다.

요청 본문이나 헤더가 상황에 따라 달라지는 경우

간헐적 실패가 네트워크 문제가 아니라 요청 데이터 문제일 수도 있습니다. 특정 사용자, 특정 상품, 특정 금액, 특정 문자 인코딩, 특정 헤더 조합에서만 실패하는 경우입니다.

예를 들어 API 인증 헤더가 일부 요청에서 누락되거나, Content-Type이 잘못 지정되거나, JSON 필드 중 null 처리 방식이 문서와 다를 수 있습니다. 이 경우 같은 API라도 요청 데이터에 따라 성공과 실패가 갈립니다.

POST /payments HTTP/1.1
Host: api.example.com
Authorization: Bearer {token}
Content-Type: application/json
Idempotency-Key: order-20260530-0001

외부 API 로그를 남길 때는 민감정보를 제외하되, 요청을 식별할 수 있는 값은 남겨야 합니다. traceId, orderId, requestId, idempotency key 같은 값이 있어야 상대 API 제공사와도 대화가 됩니다.

 

HTTP API 실패 로그를 남길 때 꼭 필요한 정보

HTTP API 실패 분석에서 로그 품질은 매우 중요합니다. 실패가 발생했는데 상태 코드와 예외 메시지만 남아 있으면 원인 추적이 어렵습니다. 반대로 요청 전문과 응답 전문을 아무 필터 없이 남기면 보안 문제가 생길 수 있습니다.

실무에서는 아래 정보 정도는 구조화해서 남기는 편이 좋습니다. 특히 외부 API 호출은 내부 요청과 다르게 상대 시스템의 로그와 맞춰봐야 하므로 식별자가 중요합니다.

{
  "event": "external_api_failed",
  "apiName": "paymentApproval",
  "method": "POST",
  "url": "https://api.example.com/payments",
  "statusCode": 504,
  "errorType": "READ_TIMEOUT",
  "durationMs": 5000,
  "traceId": "9f8a7c...",
  "requestId": "order-20260530-0001",
  "retryCount": 1
}

URL 전체를 남길 때는 query string에 토큰이나 개인정보가 포함되지 않는지 확인해야 합니다. Authorization 헤더, 주민번호, 전화번호, 카드번호, 액세스 토큰은 로그에 그대로 남기면 안 됩니다. 로그는 분석 도구이기도 하지만, 잘못 남기면 위험한 데이터 저장소가 될 수 있습니다.

 

외부 API 재시도는 언제 안전한가

HTTP API 호출 실패를 보면 가장 먼저 재시도를 떠올리기 쉽습니다. 하지만 재시도는 실패를 줄이는 도구이면서 동시에 중복 처리를 만들 수 있는 도구입니다. 특히 상태를 변경하는 API에서는 더 신중해야 합니다.

GET 요청은 비교적 재시도하기 쉽다

조회 API처럼 서버 상태를 변경하지 않는 요청은 일반적으로 재시도하기 쉽습니다. 물론 상대 API의 제한 정책은 고려해야 하지만, 동일 요청을 다시 보내도 데이터가 중복 생성되지는 않습니다.

GET /orders/1004
GET /users/42/profile
GET /delivery/tracking/ABC123

다만 GET 요청이라도 응답 데이터가 자주 바뀌거나, 호출 자체가 과금 대상인 API라면 무제한 재시도는 피해야 합니다. 재시도 횟수와 간격을 제한하고, 실패율이 높아질 때는 빠르게 중단하는 기준이 필요합니다.

POST 요청은 멱등성 보장이 중요하다

결제 승인, 주문 생성, 포인트 적립처럼 상태를 변경하는 POST 요청은 재시도 전에 멱등성을 확인해야 합니다. 요청이 실패로 보였지만 실제로는 상대 서버에서 처리되었을 수 있기 때문입니다.

이때 Idempotency-Key 같은 중복 방지 키를 사용할 수 있다면 적극적으로 사용하는 편이 낫습니다. 같은 키로 같은 요청이 여러 번 들어와도 상대 서버가 한 번만 처리해 준다면 재시도 위험을 크게 줄일 수 있습니다.

POST /payments
Idempotency-Key: payment-order-1004

{
  "orderId": "1004",
  "amount": 30000
}

만약 상대 API가 멱등성 키를 지원하지 않는다면 재시도 전략을 더 보수적으로 가져가야 합니다. 실패 후 즉시 재시도하기보다 상태 조회 API로 처리 여부를 확인하는 방식이 더 안전할 수 있습니다.

 

Spring Boot에서 외부 API 호출 코드를 작성할 때의 기준

Spring Boot에서 외부 HTTP API를 호출할 때는 단순히 호출 코드만 작성하기보다 타임아웃, 예외 처리, 로깅, 재시도 기준을 함께 설계해야 합니다. 코드가 짧아도 운영 중 분석할 수 없는 형태라면 유지보수 단계에서 불편해집니다.

아래 예시는 WebClient를 사용할 때 타임아웃과 에러 처리를 명시하는 간단한 형태입니다. 실제 프로젝트에서는 API별 클라이언트를 분리하고, 공통 로깅 필터나 관측 지표를 붙이는 방식으로 확장할 수 있습니다.

HttpClient httpClient = HttpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
    .responseTimeout(Duration.ofSeconds(5));

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .baseUrl("https://api.example.com")
    .build();

PaymentResponse response = webClient.post()
    .uri("/payments")
    .header("Idempotency-Key", request.getOrderId())
    .bodyValue(request)
    .retrieve()
    .bodyToMono(PaymentResponse.class)
    .block();

이 코드에서 중요한 부분은 connect timeoutresponse timeout을 분리했다는 점입니다. 연결 자체가 안 되는 문제와 응답이 늦는 문제는 원인이 다를 수 있기 때문입니다.

또 하나는 멱등성 키를 요청에 포함한 부분입니다. 결제처럼 중복 처리가 치명적인 API에서는 이 값이 단순한 헤더가 아니라 안정성을 위한 계약에 가깝습니다. 상대 API가 지원한다면 반드시 사용하고, 지원하지 않는다면 별도의 중복 방지 흐름을 내부적으로 갖춰야 합니다.

 

간헐적 API 실패를 추적하는 순서

HTTP API 간헐적 실패를 분석할 때는 감으로 접근하지 않는 것이 중요합니다. 아래 순서대로 확인하면 적어도 같은 원인을 반복해서 놓치는 일은 줄일 수 있습니다.

실패 요청의 공통점을 찾는다

먼저 실패한 요청이 특정 API에만 몰려 있는지, 특정 시간대에 집중되는지, 특정 서버 인스턴스에서만 발생하는지 확인합니다. 사용자, 상품, 금액, 지역, 요청 크기 같은 데이터 조건도 함께 봐야 합니다.

여기서 공통점이 보이면 분석 범위가 크게 줄어듭니다. 반대로 공통점이 없다면 네트워크, 커넥션 풀, 외부 API 제한 정책처럼 환경적인 요인으로 범위를 넓혀야 합니다.

예외 메시지를 단계별로 해석한다

예외 메시지는 단서일 뿐 결론은 아닙니다. connect timed out인지, read timed out인지, connection reset인지에 따라 요청이 어디까지 진행되었는지 추정해야 합니다.

특히 connection reset은 원인이 다양합니다. 상대 서버가 끊었을 수도 있고, 중간 장비가 끊었을 수도 있으며, 클라이언트가 오래된 커넥션을 재사용했을 수도 있습니다. 메시지 하나만 보고 책임 구간을 단정하지 않는 편이 안전합니다.

상대 API 제공사와 맞춰볼 식별자를 준비한다

외부 API 문제는 내부 로그만으로 끝나지 않는 경우가 많습니다. 상대 API 제공사에 문의하려면 요청 시간, 요청 ID, API 경로, 응답 코드, 에러 메시지, 가능하다면 상대가 발급한 transaction id가 필요합니다.

이 값들이 없으면 “몇 시쯤 실패했습니다” 수준의 문의가 됩니다. 그러면 상대도 로그를 찾기 어렵고, 결국 분석이 길어집니다. 외부 연동 기능을 만들 때부터 문의 가능한 로그 구조를 잡아두는 것이 좋습니다.

 

외부 API 장애를 줄이는 설계 포인트

HTTP API 간헐적 실패를 완전히 없애기는 어렵습니다. 외부 시스템, 네트워크, 인증서, 제한 정책, 배포 상황이 모두 우리 통제 안에 있지는 않기 때문입니다. 대신 실패가 발생했을 때 영향을 줄이고, 원인을 빠르게 찾을 수 있게 설계할 수는 있습니다.

외부 API별 클라이언트를 분리한다

모든 외부 API를 하나의 공통 HTTP 클라이언트로 처리하면 처음에는 편합니다. 하지만 시간이 지나면 API별 타임아웃, 헤더, 인증 방식, 재시도 정책이 달라집니다. 이 차이가 커지면 공통 코드가 오히려 복잡해집니다.

결제 API, 메시지 API, 조회 API처럼 성격이 다른 연동은 클라이언트를 분리하는 편이 유지보수에 유리합니다. 같은 WebClient를 쓰더라도 설정과 책임은 나눠두는 방식입니다.

실패를 비즈니스 상태로 관리한다

외부 API 실패를 단순 예외로만 처리하면 나중에 복구하기 어렵습니다. 예를 들어 결제 승인 요청이 실패했는데 주문 상태가 그대로 멈춰 있다면, 사용자는 결제가 됐는지 안 됐는지 알기 어렵습니다.

이런 경우에는 요청 중, 성공, 실패, 확인 필요 같은 상태를 별도로 관리하는 편이 낫습니다. 외부 API가 애매하게 실패했을 때는 즉시 최종 실패로 처리하기보다, 후속 조회나 수동 확인이 가능한 상태로 남겨야 합니다.

재시도와 서킷 브레이커를 구분한다

재시도는 일시적인 실패를 흡수하는 방법입니다. 반면 서킷 브레이커는 계속 실패하는 외부 API로 요청을 보내는 것을 잠시 멈추는 방법입니다. 둘은 목적이 다릅니다.

상대 API가 잠깐 흔들리는 상황에서는 제한된 재시도가 도움이 됩니다. 하지만 이미 실패율이 높아진 상태에서 모든 요청을 계속 재시도하면 오히려 외부 API와 우리 시스템 모두에 부담이 됩니다. 이때는 일정 시간 빠르게 실패시키고, 복구 여부를 확인하는 흐름이 더 낫습니다.

 

마무리: HTTP API 실패는 책임 구간을 나누어 봐야 합니다

외부 API 호출 시 간헐적 실패가 발생하면 먼저 실패 지점을 나눠야 합니다. 연결 실패인지, 응답 대기 중 실패인지, 응답 처리 실패인지 구분하지 않으면 분석이 흐려집니다.

그다음에는 타임아웃, 커넥션 풀, 요청 헤더, 응답 코드, 재시도 정책, 멱등성, 네트워크 구간을 차례로 확인해야 합니다. 특히 상태를 변경하는 API에서는 재시도가 항상 정답이 아닙니다. 요청이 실제로 처리되었는지 확인할 수 있는 기준이 먼저 필요합니다.

외부 HTTP API 연동은 코드를 한 번 작성하고 끝나는 기능이 아닙니다. 실패를 전제로 타임아웃을 정하고, 로그를 남기고, 재시도 기준을 제한하고, 애매한 상태를 복구할 수 있게 만들어야 합니다. 그래야 간헐적 실패가 발생했을 때도 원인을 좁히고 서비스 영향을 줄일 수 있습니다.

정리하면 외부 API 실패 분석의 기준은 다음과 같습니다.

  • connect timeout과 read timeout을 구분합니다.
  • connection reset은 원인을 단정하지 않고 네트워크와 커넥션 재사용을 함께 봅니다.
  • 상태 변경 API는 멱등성 없이 무조건 재시도하지 않습니다.
  • 로그에는 API명, 상태 코드, 소요 시간, 요청 식별자, 재시도 횟수를 남깁니다.
  • 외부 API별로 타임아웃, 재시도, 인증, 로깅 정책을 분리합니다.

HTTP API 호출 실패는 피할 수 없는 문제에 가깝습니다. 중요한 것은 실패 자체보다 실패를 해석하고 복구할 수 있는 구조입니다. 이 기준을 코드와 로그에 반영해두면, 다음에 비슷한 문제가 생겼을 때 훨씬 빠르게 원인을 좁힐 수 있습니다.