에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/분산 & 운영

[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산

반응형
[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산

[분산시스템] 타임아웃과 재시도 전략: 재시도 폭풍, 지수 백오프와 지터, 타임아웃 예산

재시도는 분산 시스템의 기본 회복 수단인데, 잘못 쓰면 장애를 키우는 증폭기가 돼요. 과부하로 비틀거리는 서비스에 모든 호출자가 일제히 3배의 재시도 트래픽을 퍼붓는 — 재시도 폭풍이에요. 그 폭풍을 막는 표준 처방(지수 백오프+지터)과 짝꿍 개념인 타임아웃 예산을 짚으며 분산 패턴 시리즈를 마무리해요.

01. 재시도 폭풍, 선의가 DDoS가 된다

재시도 폭풍 구조. 과부하된 서비스에 모든 호출자가 실패 후 일제히 재시도하면 트래픽이 몇 배로 증폭되어 회복하려던 상대가 더 깊이 무너진다. 고정 간격 재시도는 같은 박자의 부하 파도까지 만든다

상대가 과부하로 실패하기 시작하면, 호출자들은 "일시 장애겠지" 하고 재시도해요. 전원이 3회씩 재시도하면 트래픽이 순간 3~4배가 돼요. 평소 부하도 못 버티던 상대는 더 깊이 무너지고 회복이 불가능해져요. 선의의 재시도가 사실상 DDoS가 되는 거예요.

고정 간격 재시도는 한 술 더 떠요 — 같은 순간 실패한 호출자들이 정확히 1초 후 일제히 다시 때리고 또 일제히... 부하가 파도처럼 박자를 맞춰 몰려요.

02. 처방, 백오프 + 지터 + 한도

재시도 표준 처방. 지수 백오프로 실패할수록 간격을 늘려 상대에게 회복 시간을 주고, 지터로 호출자마다 간격을 무작위로 흩어 파도를 분산하며, 횟수는 2-3회로 제한하고 그 이상은 서킷 브레이커가 이어받는다
  • 지수 백오프 — 1초 → 2초 → 4초로 점점 물러나요. 일시 장애는 첫 재시도에 잡히고 진짜 장애엔 부하를 빠르게 줄여요.
  • 지터(jitter) — 간격에 무작위를 섞어요(예: 0~백오프값 사이 랜덤). 호출자들의 박자를 흩어 파도를 평탄하게 만들어요. 캐시 TTL jitter와 정확히 같은 원리예요 — "동시에 몰리는 것"이 적이면 무작위가 약이에요.
  • 횟수 제한 — 2~3회면 충분해요. 그걸로 안 되면 일시 장애가 아니라는 뜻이고 그때부턴 서킷 브레이커가 이어받아 아예 차단하는 게 맞아요. 재시도(짧은 장애용)와 서킷(긴 장애용)은 역할 분담이에요.
// Resilience4j Retry — 백오프 + 지터
RetryConfig config = RetryConfig.custom()
    .maxAttempts(3)   // 최초 호출 포함 3회 = 재시도는 2회
    .intervalFunction(IntervalFunction.ofExponentialRandomBackoff(
        Duration.ofMillis(500), 2.0, 0.5))   // 0.5s부터 2배씩, ±50% 지터
    .retryExceptions(IOException.class, TimeoutException.class)
    .ignoreExceptions(BusinessException.class)   // 비즈니스 실패는 재시도 무의미
    .build();
가장 중요한 전제 — 재시도는 멱등한 작업에만. "타임아웃 = 실패"가 아니라 "결과 모름"이라는 걸 기억하세요(멱등 API 편의 그 비대칭). 처리됐는데 응답만 유실된 호출을 재시도하면 중복 실행이에요. 결제 호출의 재시도는 Idempotency-Key가 깔려 있을 때만 안전하고 그게 없다면 재시도 대신 "상태 조회 후 판단"이 맞아요. 그리고 비즈니스 실패(잔액 부족)는 백 번 재시도해도 똑같으니 재시도 대상에서 빼요 — 카프카 에러 분류의 일시적/영구적 구분이 여기서도 그대로예요.

03. 타임아웃 예산, 바깥보다 안쪽이 짧아야

재시도와 한 몸인 게 타임아웃이에요. 타임아웃 없는 재시도는 "언제 실패할지"가 없어 성립하지 않고 타임아웃 값들이 서로 안 맞으면 또 다른 사고가 나요.

타임아웃 예산 개념. 클라이언트나 게이트웨이의 전체 타임아웃 안에 내부 호출들과 재시도 여유가 들어와야 한다. 안쪽 타임아웃이 바깥보다 길면 클라이언트가 떠난 뒤에도 서버가 일하고 중복 처리가 생긴다

원칙은 예산 배분이에요. 게이트웨이가 3초에 끊는다면, 그 안의 내부 호출들 + 재시도 시간의 합이 3초 안에 들어와야 해요. 흔한 사고는 반대예요 — 안쪽 외부 API 타임아웃이 10초인데 바깥이 3초면, 클라이언트는 3초에 떠나서(어쩌면 벌써 재시도해서) 응답 받을 사람이 없는데 서버는 7초를 더 일해요. 자원 낭비에 중복 처리까지 겹치죠.

실무 규칙으로 정리하면:

  • 모든 외부 호출에 명시적 타임아웃 — 기본값(무한대거나 너무 긴)을 믿지 않아요. HTTP 클라이언트의 connect/read 타임아웃, Redis, DB까지 전부요.
  • 안쪽 합(재시도 포함) < 바깥 — 재시도 1회를 계획한 호출이라면 (타임아웃 × 2 + 백오프)가 예산 안에 들어와야 해요.
  • 타임아웃은 p99 기준으로 — 평균이 아니라 정상 응답의 99퍼센타일보다 약간 크게. 너무 짧으면 멀쩡한 요청을 죽이고(자체 오탐), 너무 길면 장애 때 자원이 묶여요.

04. 어디서 재시도할 것인가

마지막 설계 질문 — 재시도를 어느 층에서 하느냐예요. 클라이언트도, 게이트웨이도, 내 서비스도, 메시지큐 컨슈머도 재시도를 하면 곱셈이 일어나요. 3회 × 3회 × 3회 = 27배 증폭이죠.

원칙은 "재시도는 한 층에서만"이에요. 보통 실패의 의미를 가장 잘 아는 층이 맡아요 — 외부 API 호출이면 그걸 부르는 서비스가, 메시지 처리면 컨슈머의 재시도+DLQ가요. 나머지 층은 재시도 없이 실패를 그대로 전달하고 최종 사용자 층에서만 "다시 시도" 버튼으로 사람의 판단에 맡겨요.

05. 자주 만나는 문제

장애가 났는데 트래픽이 오히려 몇 배로 뛰어요

재시도 폭풍이에요. 백오프+지터를 깔고 층층이 재시도가 중첩돼 있지 않은지(곱셈) 점검해요.

재시도 때문에 중복 처리가 생겨요

비멱등 작업의 재시도예요. Idempotency-Key(1편)를 깔거나 재시도 대신 조회-후-판단으로 바꿔요.

타임아웃을 줄였더니 멀쩡한 요청이 실패해요

p99보다 짧게 잡은 거예요. 실측 분포를 보고 정하고 느린 의존성 자체의 개선과 병행해요.

정리 — 그리고 시리즈를 닫으며

재시도의 공식은 멱등 전제 + 지수 백오프 + 지터 + 2~3회 한도 + (그 너머는 서킷)이고 타임아웃은 전 구간 명시 + 안쪽이 바깥보다 짧게예요. 그리고 재시도는 한 층에서만 해요.

이걸로 분산 패턴 시리즈가 마무리돼요. 멱등 API(중복을 무해하게), 아웃박스(전달을 확실하게), 사가(흐름의 정합성), 서킷 브레이커(장애 격리), 그리고 타임아웃과 재시도(회복의 규율) — 다섯 조각이 서로를 전제하며 맞물려요. 그 토대는 결국 카프카·Redis·JPA·MySQL 시리즈에서 다진 한 가지 감각, 분산된 것들 사이의 틈을 의심하고 그 틈을 설계로 메우는 일이에요.

다음 글부터는 이런 큰 그림이 아니라 코드 한 줄짜리 함정들이에요. 돈 계산에 double을 쓰면 안 되는 이유부터 시작해요.

출처: AWS Builders' Library — Timeouts, retries, and backoff with jitter · Resilience4j — Retry

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."