“재시도하면 되죠” API 장애를 더 키우는 설정
결제 요청이 실패하자 개발자가 재시도 횟수를 늘렸는데, 잠시 뒤 서버가 완전히 멈췄습니다. 외부 API가 느려진 상황에서 모든 클라이언트가 같은 간격으로 요청을 반복하면 복구를 돕기는커녕 재시도 폭주를 일으킬 수 있습니다.
API 장애는 대형 서비스만의 문제가 아닙니다. 결제, 문자 발송, 지도, 생성형 AI처럼 외부 기술에 연결된 스타트업 서비스라면 작은 설정 하나가 중복 결제와 데이터 불일치로 이어질 수 있습니다. 디지털 서비스의 범위를 이해하려면 디지털의 개념을 설명한 지식백과도 함께 참고할 만합니다.
재시도가 장애 복구를 방해하는 순간
실패한 요청을 곧바로 다시 보내면 생기는 일
요청 한 건이 실패했을 때 즉시 다시 보내는 행동은 자연스러워 보입니다. 하지만 서버가 감당할 수 있는 처리량을 이미 넘어선 상태라면 재시도 요청은 새로운 부하가 됩니다. 사용자 1,000명이 각각 3회씩 다시 요청하면 서버 입장에서는 원래보다 몇 배 많은 작업이 한꺼번에 밀려옵니다.
특히 모든 클라이언트가 1초, 2초, 3초처럼 동일한 간격으로 재시도하면 요청이 파도처럼 반복됩니다. 서버가 잠깐 여유를 찾는 순간에도 다시 트래픽이 몰리기 때문에 정상 상태로 돌아갈 틈이 없습니다. 이를 피하려면 지수 백오프와 지터를 함께 적용해야 합니다.
지수 백오프는 실패할 때마다 대기 시간을 1초, 2초, 4초, 8초처럼 늘리는 방식입니다. 지터는 여기에 무작위 시간을 더해 여러 사용자의 요청이 같은 시점에 몰리지 않게 합니다. 예를 들어 4초 대기 구간이라면 각 요청을 3.2초에서 5.1초 사이로 분산할 수 있습니다.
- 즉시 재시도: 네트워크가 순간적으로 끊긴 경우에는 도움이 되지만 서버 과부하 상황에서는 위험합니다.
- 고정 간격 재시도: 구현은 간단하지만 다수의 클라이언트가 동시에 움직이는 동기화 현상을 만들기 쉽습니다.
- 지수 백오프와 지터: 서버에 회복 시간을 주고 재요청 시점을 분산합니다.
- 무제한 재시도: 사용자 요청이 끝난 뒤에도 작업이 쌓일 수 있으므로 반드시 최대 횟수나 총 제한 시간을 둬야 합니다.
재시도는 성공률을 높이는 버튼이 아니라 서버에 추가 작업을 요청하는 정책입니다. 횟수보다 먼저 ‘어떤 실패에 다시 요청할 것인가’를 결정해야 합니다.
재시도해도 되는 오류부터 구분합니다
모든 오류가 재시도 대상은 아닙니다. HTTP 400처럼 요청 형식이 잘못된 오류는 같은 데이터를 다시 보내도 대부분 실패합니다. 인증 정보가 틀린 401, 접근 권한이 없는 403 역시 토큰 갱신이나 권한 수정 없이 반복할 이유가 없습니다.
반면 429는 일정 시간 동안 요청이 너무 많았다는 신호이며, 502·503·504는 일시적인 게이트웨이 또는 서버 문제일 가능성이 있습니다. 다만 상태 코드만 믿지 말고 API 제공사가 안내하는 오류 문서와 Retry-After 헤더를 확인해야 합니다.
- 400·422: 입력값을 검증하고 사용자 또는 호출 코드에 오류를 반환합니다.
- 401: 토큰 갱신을 한 번 시도한 뒤 동일 오류가 나면 로그인을 다시 요구합니다.
- 403·404: 권한과 주소를 확인하며 자동 재시도는 기본적으로 중단합니다.
- 429: Retry-After가 있으면 해당 시간을 우선하고, 없으면 백오프를 적용합니다.
- 502·503·504: 짧은 장애일 수 있으므로 제한된 횟수로 분산 재시도합니다.
중복 결제를 막는 멱등성 설계
응답이 없었다고 처리도 실패한 것은 아닙니다
사용자가 결제 버튼을 눌렀고 화면에는 시간 초과가 표시됐다고 가정해 보겠습니다. 결제 서버는 승인을 끝냈지만 응답이 돌아오는 네트워크 구간에서 연결이 끊겼을 수 있습니다. 이때 앱이 새로운 결제 요청을 보내면 한 번의 구매가 두 건으로 처리되는 문제가 생깁니다.
해결의 핵심은 요청마다 고유한 멱등성 키를 부여하는 것입니다. 서버는 같은 키로 들어온 요청을 다시 처리하지 않고 처음 저장한 결과를 돌려줍니다. 사용자가 버튼을 두 번 누르거나 앱이 자동 재시도하더라도 업무상 결과는 한 번만 만들어집니다.
키는 사용자의 클릭마다 새로 만들되, 같은 결제 작업을 재시도하는 동안에는 유지해야 합니다. 단순한 현재 시각보다 UUID처럼 충돌 가능성이 낮은 값을 사용하고, 서버에서는 키와 요청 본문의 해시, 처리 상태, 응답 값을 함께 저장하는 편이 안전합니다.
- 사용자가 주문을 확정하면 클라이언트 또는 백엔드가 고유 요청 키를 생성합니다.
- 서버는 키가 처음 들어왔는지 저장소에서 확인합니다.
- 처리 중인 키라면 새 작업을 만들지 않고 진행 상태를 반환합니다.
- 완료된 키라면 저장해 둔 성공 또는 실패 응답을 그대로 반환합니다.
- 같은 키에 다른 금액이나 상품이 들어오면 재사용 오류로 차단합니다.
버튼 잠금만으로는 부족한 이유
결제 버튼을 누른 뒤 비활성화하는 UI는 중복 클릭을 줄여주지만 완전한 해결책은 아닙니다. 사용자가 화면을 새로 고치거나 모바일 앱과 웹을 동시에 사용할 수 있고, 중간 프록시나 작업 큐가 요청을 다시 전달할 수도 있기 때문입니다. 최종 방어선은 반드시 서버에 있어야 합니다.
주문 생성, 쿠폰 사용, 포인트 차감처럼 한 번만 실행돼야 하는 기능에도 같은 원리를 적용할 수 있습니다. IT 시스템이 정보의 생성과 처리, 전달을 함께 다룬다는 배경은 IT 용어 설명에서 확인할 수 있습니다. 화면의 성공 여부와 서버의 실제 처리 상태를 분리해 생각하는 습관이 중요합니다.
- 결제·주문·예약 생성에는 멱등성 키를 필수로 사용합니다.
- 버튼 비활성화는 사용자 경험 개선 수단으로만 취급합니다.
- 키 보관 기간은 업무상 중복 요청이 들어올 수 있는 시간보다 길게 설정합니다.
- 요청 본문이 달라졌다면 같은 키를 받아들이지 않습니다.
- 처리 중 서버가 종료돼도 복구할 수 있도록 상태 전이를 저장합니다.
타임아웃과 서킷 브레이커를 함께 조정하는 법
연결 시간과 응답 시간을 따로 제한합니다
타임아웃을 30초 하나로만 두면 어디에서 시간이 소비됐는지 알기 어렵습니다. 연결 자체가 되지 않는 상황과 연결 후 응답이 늦어지는 상황은 원인도 대응법도 다릅니다. 연결 타임아웃, 읽기 타임아웃, 전체 요청 제한 시간을 나눠 설정해야 합니다.
예를 들어 사용자 화면이 5초 안에 답해야 하는데 내부 API가 10초를 기다리도록 돼 있다면 구조적으로 목표를 달성할 수 없습니다. 프런트엔드 5초, 백엔드 A 8초, 백엔드 B 10초처럼 안쪽으로 갈수록 제한이 길어지는 설정도 위험합니다. 호출 체인의 가장 안쪽 서비스가 먼저 포기하고 바깥 서비스가 대체 응답을 만들 수 있도록 타임아웃 예산을 배분해야 합니다.
다음은 3초 안에 사용자에게 결과를 보여줘야 하는 서비스의 예시입니다. 절대값을 그대로 복사하기보다는 실제 응답 시간의 95·99백분위와 사용자 경험 목표를 기준으로 조정하세요.
| 구간 | 예시 제한 | 설정 목적 |
|---|---|---|
| DNS·연결 | 300~500ms | 접속 불가능한 대상에서 빠르게 이탈 |
| 외부 API 응답 | 1.2~1.8초 | 대체 경로를 실행할 시간 확보 |
| 전체 백엔드 처리 | 2.2초 이내 | 화면 응답과 로그 기록 시간 보존 |
| 클라이언트 표시 | 3초 이내 | 사용자에게 진행 상태 또는 대안 제공 |
- 평균 응답 시간만 보지 말고 느린 요청이 드러나는 상위 백분위를 확인합니다.
- 긴 작업은 웹 요청 안에서 끝내려 하지 말고 작업 큐로 넘깁니다.
- 타임아웃 발생 시 요청 ID, 대상 API, 경과 시간과 재시도 횟수를 기록합니다.
- 사용자 화면에는 무작정 로딩 표시를 유지하지 말고 조회 가능한 처리 번호를 제공합니다.
계속 실패하는 API의 문을 잠시 닫습니다
외부 번역 API가 10분째 실패하는데 요청마다 연결을 시도한다면 애플리케이션의 스레드와 연결 풀까지 고갈될 수 있습니다. 서킷 브레이커는 일정 비율 이상 실패했을 때 호출을 잠시 차단하고, 캐시나 기본값 같은 대체 경로로 보내는 장치입니다.
닫힘 상태에서는 정상 호출이 진행되고, 실패 기준을 넘으면 열림 상태로 전환됩니다. 대기 시간이 지난 뒤에는 일부 요청만 보내 회복 여부를 확인하는 반열림 상태가 됩니다. 이때 기준을 너무 민감하게 잡으면 작은 흔들림에도 기능이 차단되고, 너무 느슨하게 잡으면 이미 장애가 난 서비스에 호출이 계속 쌓입니다.
- 최근 20건 중 10건 실패처럼 최소 표본과 실패 비율을 함께 정합니다.
- 결제처럼 중요한 기능은 단순 기본값 대신 처리 대기 상태를 제공해야 합니다.
- 검색 추천처럼 부가 기능은 인기 항목이나 캐시 결과로 우회할 수 있습니다.
- 반열림 상태에서는 소수의 시험 요청만 허용해 재폭주를 막습니다.
- 차단 횟수와 대체 경로 사용률을 별도 지표로 관찰합니다.
장애 대응 설정은 코드에 한 번 넣고 끝나는 값이 아닙니다. 실제 트래픽에서 타임아웃, 재시도 성공률, 중복 차단 건수를 함께 보며 조정해야 합니다.
오후 2시 문자 발송 장애를 복구한 주문팀
한 주문을 따라가며 설정을 바꿔봅니다
직원 12명의 쇼핑 스타트업에서 오후 2시 할인 행사를 시작했습니다. 주문 완료 문자를 보내는 외부 API가 느려지자 주문 API의 응답 시간이 평소 400ms에서 9초로 늘었습니다. 애플리케이션은 문자 발송이 실패할 때마다 1초 간격으로 다섯 번 재시도했고, 연결 풀을 모두 사용하면서 문자와 관계없는 주문 조회까지 느려졌습니다.
운영팀은 먼저 문자 발송을 주문 처리 경로에서 분리했습니다. 결제와 주문 저장이 끝나면 사용자에게 주문 번호를 반환하고, 문자 작업은 큐에서 비동기로 처리하도록 바꿨습니다. 문자 API에는 연결 400ms, 읽기 1.5초의 타임아웃을 적용하고 최대 세 번만 지수 백오프로 재시도했습니다.
재시도 대기 시간은 2초, 4초, 8초를 기준으로 삼되 각각 무작위 지터를 더했습니다. 같은 주문 번호와 메시지 유형으로 발송 키를 만들어 큐가 작업을 다시 전달해도 문자가 한 번만 발송되도록 했습니다. 실패한 작업은 삭제하지 않고 별도 보관함으로 이동해 운영자가 원인과 재처리 여부를 확인하게 했습니다.
- 14:05: 문자 API의 5xx 오류율이 임계값을 넘자 서킷 브레이커가 열렸습니다.
- 14:06: 신규 주문은 정상 저장됐고 화면에는 “주문은 완료됐으며 알림이 지연될 수 있습니다”라는 문구가 표시됐습니다.
- 14:10: 대기 작업에는 주문 번호 기반 멱등성 키가 있어 중복 발송 위험 없이 보존됐습니다.
- 14:17: 반열림 상태에서 시험 요청 다섯 건 중 다섯 건이 성공해 호출이 단계적으로 재개됐습니다.
- 14:24: 대기열 처리 속도를 제한해 복구 직후 외부 API로 요청이 한꺼번에 쏟아지는 상황을 피했습니다.
이 사례에서 중요한 변화는 재시도 횟수를 단순히 줄인 것이 아닙니다. 주문 완료와 문자 발송의 성공 조건을 분리하고, 실패해도 데이터가 사라지지 않는 경로를 만든 것입니다. 고객은 결제를 다시 시도할 필요가 없었고 운영자는 어떤 주문의 알림이 늦었는지 정확히 조회할 수 있었습니다.
같은 방식은 이메일, 푸시 알림, 생성형 AI 요약, 송장 등록에도 적용할 수 있습니다. 먼저 사용자가 기다려야 하는 핵심 처리와 나중에 실행해도 되는 부가 처리를 나누세요. 그다음 타임아웃과 재시도, 멱등성 키, 서킷 브레이커를 각각 연결하면 API 장애가 서비스 전체로 번지는 경로를 끊을 수 있습니다.
- 배포 전 테스트 환경에서 지연, 연결 거절, 429, 503 응답을 의도적으로 발생시킵니다.
- 재시도 횟수보다 최종 성공률과 사용자 대기 시간을 함께 측정합니다.
- 대기열이 늘어날 때 알림을 받고 소비 속도를 조절할 운영 기준을 마련합니다.
- 장애 공지에는 기술 용어보다 영향받는 기능과 사용자가 취할 행동을 명확히 적습니다.
- 복구 직후에는 쌓인 작업을 한꺼번에 실행하지 말고 처리량을 단계적으로 높입니다.

- 다음글스타트업 데이터 백업을 한 달 운영해봤더니 드러난 6가지 실수 26.08.19
등록된 댓글이 없습니다.
