현대 서비스는 점점 더 많은 외부 시스템과 연동되어 운영된다. 결제 시스템, 알림 서비스, 지도 API, 소셜 로그인 등 서비스 하나가 정상 동작하려면 수많은 외부 의존성이 제대로 작동해야 한다. 문제는 이 의존성 중 하나라도 장애가 발생하면, 그 영향이 우리 서비스 전체로 퍼질 수 있다는 점이다.
하지만 외부 연동은 완전히 통제할 수 없다. 상대 서비스가 느려지거나 일시적으로 다운되거나 네트워크가 불안정해지는 상황은 언제든 발생할 수 있다. 하지만 그 영향을 최소화하는 것은 우리가 할 수 있는 일이다.
타임아웃은 외부 연동에서 가장 먼저, 그리고 반드시 설정해야 하는 항목이다. 타임아웃을 설정하지 않으면, 상대 서비스가 응답하지 않거나 느려질 경우 우리 서비스의 스레드가 무한정 대기 상태에 빠지게 된다.
쇼핑몰에서 주문 완료 후 외부 포인트 적립 API를 호출한다고 가정해 보자. 어느 날 포인트 서비스에 과부하가 발생해 응답이 느려지기 시작했다.
이와 같이 포인트 서비스 하나의 장애가 전체 서비스 다운으로 이어지는 상황이 발생할 수 있는데, 타임아웃이 설정되어 있다면 설정한 시간이 지난 후 에러를 반환하고 스레드를 반환하게 된다. 사용자는 에러 메시지를 보게 되지만, 다른 기능들은 정상적으로 동작할 수 있다.

타팀아웃에는 두 가지 종류가 있으며 각각 다른 시점에 동작한다.
| 구분 | 발생 시점 | 설명 |
|---|---|---|
| 연결 타임아웃 | TCP 연결 시도 단계 | 상대 서버가 존재하지 않거나 네트워크 경로에 문제가 있을 때 발생한다. |
| 예) 연결 시도 후 3초가 지나도 연결이 안 되면 에러 처리. | ||
| 읽기 타임아웃 | 연결 완료 후 응답 대기 단계 | 상대 서버는 살아있지만 처리가 느릴 때 발생한다. |
| 예) 연결 성공 후 응답을 10초 동안 기다려도 오지 않으면 에러 처리. |
처음 외부 서비스와 연동할 때는 아래 값으로 시작한 뒤, 실제 운영 데이터를 보며 점진적으로 조정하는 것이 좋다.
| 타임아웃 유형 | 권장 초기값 | 이유 |
|---|---|---|
| 연결 타임아웃 | 3초 ~ 5초 | 대부분의 경우 네트워크 연결은 1초 안에 완료된다. 3~5초가 지나도 연결되지 않으면 문제가 있는 것으로 판단한다. |
| 읽기 타임아웃 | 5초 ~ 30초 | 처리 시간은 API마다 편차가 크다. 처음부터 너무 짧게 설정하면 정상 처리 중에 타임아웃 에러가 발생할 수 있다. |
| 결제 API 읽기 타임아웃 | 30초 이상 권장 | 결제처럼 민감한 API는 간헐적 지연이 발생해도 정상 처리가 되어야 하므로 넉넉하게 설정한다. |
⚠️ 주의: 타임아웃 값이 너무 짧으면 오탐이 발생할 수 있다.
읽기 타임아웃을 1~2초로 설정하면 상대 서비스가 정상적으로 처리 중임에도 타임에러가 발생할 수 있다. 특히 결제, 예약처럼 실제 처리 시간이 긴 API는 충분히 여유를 두는 것이 좋다.
재시도는 일시적인 실패를 성공으로 바꿀 수 있는 효과적인 방법이다. 네트워크는 본질적으로 불안정하며, 순간적인 패킷 손실이나 연결 불안정으로 인한 실패는 재시도만으로 해결되는 경우가 많다.
하지만 모든 상황에서 재시도를 해도 되는 것은 아니다. 잘못된 상황에서의 재시도는 오히려 상황을 악화시킬 수 있다.

| 상황 | 재시도 여부 | 이유 |
|---|---|---|
| 단순 조회 API | ✅ 가능 | 조회는 서버 상태를 변경하지 않으므로 여러 번 호출해도 결과가 같다. |
| 예) 상품 정보 조회, 환율 조회 | ||
| 연결 타임아웃 | ✅ 가능 | 연결 자체가 실패한 것이므로 서버는 요청을 받지도 않은 상태다. 재시도해도 중복 처리 위험 없음. |
| 멱등성 있는 변경 API | ✅ 가능 | 여러 번 호출해도 결과가 동일한 API. 예) 특정 상태로 업데이트하는 API (enabled=true로 설정) |
| 읽기 타임아웃 (변경 API) | ⚠️ 주의 | 서버가 이미 처리 중일 수 있다. 재시도하면 동일 작업이 두 번 실행될 수 있음. 멱등성 확인 필수. |
| 검증 오류 (4xx) | ❌ 불가 | 파라미터 오류, 권한 없음 등 요청 자체가 잘못된 경우. 재시도해도 같은 오류가 발생한다. |
재시도를 많이 한다고 좋은 것은 아니다. 재시도 횟수만큼 사용자의 응답 대기 시간도 늘어나기 때문이다. 일반적으로 1~2회가 적당하다.
| 전략 | 설명 |
|---|---|
| 단순 재시도 | 고정된 간격으로 N회 재시도한다. |
| 예) 3초 후 재시도, 3초 후 재시도. | |
| 지수 백오프(Exponential Backoff) | 재시도할수록 간격을 지수적으로 늘린다. |
| 예) 1초 → 2초 → 4초. 서버 부하를 점진적으로 줄이는 효과가 있다. | |
| 지터 (Jitter) 추가 | 지수 백오프에 무작위 지연을 추가한다. 여러 클라이언트가 동시에 재시도할 때 요청이 한 번에 몰리는 것을 방지한다. |
연동 서비스가 느려져서 읽기 타임아웃이 발생하는 상황을 생각해 보자. 이때 모든 클라이언트가 재시도를 하면 연동 서비스는 기존 요청을 처리하는 도중 재시도 요청까지 받게 된다.
초당 1,000건 요청 + 2회 재시도하는 시스템이 있다고 가정하자. 연동 서비스는 초당 최대 3,000건을 받게 된다. 이미 느린 서버에 3배의 트래픽이 쏟아지는 것이다. 이처럼 장애를 해결하기 위한 재시도가 오히려 상태를 더 악화시킬 수 있다.
이때 해결 방안으로는 읽기 타임아웃이 발생한 경우, 연동 서비스의 현재 상태를 먼저 확인하고 재시도 여부를 결정하는 것이다. 서킷 브레이커와 함께 사용하면 더욱 효과적이다.
연동 서비스는 처리할 수 있는 용량이 정해져 있다. 이 용량을 초과하는 요청을 보내면 연동 서비스의 응답 시간이 길어지고, 결국 우리 서비스의 응답 시간도 함께 느려지는 연쇄 장애로 이어질 수 있다.
배달 앱에서 주문 완료 시 외부 문자 발송 API를 호출한다고 가정해 보자. 문자 API는 초당 최대 500건을 처리할 수 있다. 그러던 중, 점심 피크 타임에 초당 800건의 주문이 들어오게 되며 문자 API에 800건의 요청이 동시에 전달되는 상황이 발생했다.
이 상황에서 발생한 장애를 해결하기 위해서는, 문자 API로의 동시 요청 수를 500건 이하로 제한하는 것이다. 초과 요청은 큐에 넣거나 에러를 반환하도록 설계하면, 문자 API는 안정적으로 동작하기 시작하며 타임아웃 장애가 사라진다.
동시 요청 제한의 개념을 확장한 것이 벌크헤드 패턴이다. 선박의 격벽처럼, 연동하는 서비스별로 별도의 요청 풀을 분리한다.

예를 들어 결제 API에 10개, 알림 API에 5개의 스레드를 각각 할당하면, 결제 API가 느려져서 결제용 스레드 10개가 모두 소진되더라도 알림 API용 스레드는 영향을 받지 않게 된다.
연동 서비스에 장애가 발생한 상태에서 계속 요청을 보내면, 모든 요청이 타임아웃까지 대기하다 에러를 반환하게 된다. 이는 불필요한 대기 시간과 자원 낭비를 초래한다.
가정의 누전 차단기처럼, 오류가 임계치를 초과하면 회로를 차단(Open)하고, 이후 요청은 연동 서비스에 전달하지 않고 즉시 에러를 반환한다. 이를 빠른 실패(Fail Fast)라고 한다.

서킷 브레이커 동작 방식은 다음과 같다.
설정: 오류율 임계치 50%, 대기 기간 10초, 반 열림 테스트 요청 3건
서킷이 열린 상태에서는 연동이 불가하므로, 에러 대신 대안 응답(Fallback)을 제공하면 사용자 경험을 개선할 수 있다.
| 폴백 전략 | 예시 |
|---|---|
| 기본값 반환 | 배송 예상 시간 조회 API가 실패하면 "배송 예상 시간을 표시할 수 없습니다" 메시지 반환 |
| 캐시 데이터 활용 | 실시간 환율 API가 실패하면 마지막으로 캐싱된 환율 데이터 사용 (시간 표기와 함께) |
| 기능 일부 비활성화 | 추천 상품 API가 실패하면 해당 섹션을 숨기고 나머지 페이지는 정상 표시 |
Spring 환경에서 Resilience4j 활용 예시
CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 오류율 50% 초과 시 Open
.waitDurationInOpenState(Duration.ofSeconds(10)) // Open 유지 10초
.permittedNumberOfCallsInHalfOpenState(3) // Half-Open 테스트 요청 3건
.slidingWindowSize(10) // 최근 10건 기준으로 오류율 계산
.build();
maxConcurrentCalls: 연동 서비스의 처리 용량을 파악한 뒤 80% 수준으로 설정하는 것이 권장된다.
외부 API 호출과 DB 작업을 함께 처리할 때는 오류 발생 시 데이터의 일관성이 깨질 수 있다. 어느 쪽이 먼저 성공하고 어느 쪽이 실패했는지에 따라 후처리 방법이 달라진다.
주문 처리 중 결제 API가 실패한 상황이라고 가정해 보자.
타임아웃이 발생했다고 해서 결제 서버가 반드시 실패한 것은 아니다. 결제 서버는 정상 처리했지만 응답이 늦었을 수도 있다. 이 경우 DB에는 주문이 없는데 실제로는 결제가 된 상황이 발생하게 된다.
이 경우 아래 세 가지 방법 중 하나를 검토해야 한다.
| 해결 방법 | 설명 | 전제 조건 |
|---|---|---|
| 데이터 정합성 점검 | 일정 주기(예: 1분, 5분)로 우리 DB와 결제 서버의 데이터를 비교하여 불일치를 감지하고 보정한다. | 없음 (가장 범용적) |
| 성공 확인 API 호출 | 타임아웃 발생 후 일정 시간이 지난 뒤, 결제 서버에 해당 결제가 성공했는지 조회하는 API를 호출한다. | 연동 서비스가 성공 확인 API를 제공해야 함 |
| 취소 API 호출 | 타임아웃 발생 후 결제 취소 API를 호출한다. 처리됐으면 취소하고, 처리 안 됐으면 성공 응답만 반환한다. | 취소 API가 멱등성을 지원해야 |
이번에는 결제는 성공했으나 DB에 저장 실패한 상황을 가정해 보자.
이 결과, 결제는 완료됐는데 DB에는 주문 정보가 없는 상황이 발생한다. 이 경우 성공 확인 API는 의미가 없다. 반드시 결제 취소 API를 호출해야 한다. 하지만 취소 API가 없거나 취소도 실패할 수 있다. 중요 서비스라면 이런 경우를 위한 정합성 점검 프로세스를 별도로 운영해야 한다.
DB 트랜잭션 범위 안에서 외부 API를 호출하면 외부 API 응답 대기 시간 동안 DB 커넥션이 점유된 상태로 유지된다.
다음은 DB 커넥션 풀 크기는 10이고 외부 API 읽기 타임아웃은 5초인 시스템의 DB 커넥션 풀 고갈 시나리오이다.
이 상황에서의 해결책은 DB 커넥션을 획득하기 전에 외부 연동을 먼저 실행하거나, 외부 연동을 트랜잭션 범위 밖으로 이동시킨다. 단, 트랜잭션 외부로 이동하면 롤백이 불가능해지므로, 실패 시 보상 트랜잭션이나 후보정 프로세스를 반드시 준비해야 한다.

외부 HTTP API를 호출할 때마다 TCP 연결을 새로 맺으면 불필요한 시간이 소요된다. HTTP 커넥션 풀을 사용하면 연결을 재사용하여 응답 속도를 높일 수 있다.
DB 커넥션 풀과 같은 원리이다. 미리 일정 수의 HTTP 연결을 맺어 풀에 보관하고, 요청이 들어올 때 풀에서 연결을 꺼내 사용한 뒤 반납하는 원리이다.
| 설정 항목 | 설명 | 권장 접근법 |
|---|---|---|
| 풀 크기 (Pool Size) | 연동 서비스의 처리 용량에 맞게 설정해야 하며, 과도하게 크게 설정할 경우 연동 서비스에 부하를 줄 수 있음 | 연동 서비스의 TPS를 기준으로 적정 수준 설정. 초기에는 작게 시작하고 모니터링 기반으로 점진적 조정 |
| 풀 대기 시간 | 커넥션 풀에서 커넥션을 획득하기까지 기다리는 시간. 길어질수록 전체 응답 시간 증가 | 수 초 이내로 짧게 설정. 대기 시간 초과 시 빠르게 에러 반환하도록 구성 |
| 커넥션 유지 시간 (Keep-Alive) | 서버와의 연결을 유지하는 시간. 상대 서버의 설정보다 길 경우 연결이 끊어진 상태의 커넥션을 사용할 수 있음 | 상대 서버의 Keep-Alive보다 짧게 설정하여 끊어진 커넥션 재사용 문제 방지 |
⚠️ 유효하지 않은 커넥션 사용 주의
연동 서비스가 서버 재시작이나 타임아웃으로 연결을 끊었는데, 풀에는 아직 그 커넥션이 남아 있으면 요청 시 에러가 발생할 수 있다.
따라서 풀에서 커넥션을 가져올 때 유효성 검증(Validation) 옵션을 활성화하거나, Keep-Alive 시간을 상대 서버보다 짧게 설정하여 우리 쪽에서 먼저 연결을 정리하는 방식으로 해결해야 한다.
서비스가 성장하여 대량 트래픽을 처리해야 하는 시점이 되면, 단일 외부 서비스에 대한 의존이 단일 장애점(Single Point of Failure, SPOF)이 될 수 있다. 이중화는 이런 위험을 분산하는 방법이다.
예를 들어 문자 발송 기능에 단일 SMS 벤더만 사용하는 서비스가 있다고 가정하자. 만약 벤더 서비스에 장애가 발생하면 해당 서비스의 모든 문자 발송이 중단된다. 하지만 이중화를 하게 된다면 SMS 벤더 A와 B를 모두 연동해 두고, A에 장애가 발생하면 B로 전환하여 문자 발송을 유지할 수 있게 되는 것이다.
이중화는 개발과 유지에 드는 비용이 두 배로 늘어나므로, 모든 외부 연동에 적용하기보다 아래 기준으로 선별적으로 적용해야 한다.
| 판단 기준 | 설명 |
|---|---|
| 핵심 기능 여부 | 해당 연동이 없으면 서비스의 핵심 가치가 제공되지 않는가? 예) 결제, 인증, 핵심 알림 기능. |
| 장애 영향 범위 | 연동 서비스 장애 시 영향받는 사용자 범위와 비즈니스 손실이 이중화 비용을 상회하는가? |
| SLA 요구사항 | 연동 서비스의 SLA(가용성 보장 수준)가 우리 서비스의 SLA를 충족하지 못한다면 이중화를 고려해야 한다. |
| 이중화 비용 | 두 벤더 계약 비용, 개발 비용, 지속적인 유지보수 비용을 감당할 수 있는가? |
| 방식 | 설명 | 특징 |
|---|---|---|
| Active-Standby | 평소에는 Primary만 사용하고 장애 시 Secondary로 전환한다. | 전환 시간이 필요하지만 구현이 단순하다. 서킷 브레이커로 자동 전환 구현 가능. |
| Active-Active | 두 서비스에 트래픽을 동시에 분산한다. | 가용성이 높고 부하도 분산되지만, 양쪽 데이터 동기화가 복잡하다. |
| 우선순위 기반 | Primary에 먼저 시도하고 실패 시 Secondary를 사용한다. | 서킷 브레이커와 조합하면 효과적이다. 가장 일반적인 이중화 패턴. |
