멱등성을 전제로 한 재시도: 언제 어떤 기준으로 해야할까?
빠른 요약과 정리
- 재시도는 멱등성이 보장될 때만 안전하다. 멱등키 없이 결제 승인을 재시도한다면 곧바로 이중 결제 위험이 된다.
- 재시도의 대상은
결과가 불확실한 실패다. 네트워크 타임아웃, 연결 끊김 처럼 서버 상태를 모르는 경우만 재시도하고, 비즈니스 거절처럼 결과가 확정된 실패는 재시도하지 않는다. - 재시도 시에 멱등키는 절대 바꾸지 않는다. 키를 새로 만들면 멱등성이 깨져서 중복 처리가 된다. 하의 논리적 요청 = 하나의 멱등키를 재시도 내내 유지한다.
- 정책은 백오프, 지터, 짧은 최대 횟수, 명시적 타임아웃 결제처럼 민감한 작업일수록 보수적으로 잡는다.
어떤 개념인가요 ?
멱등성
같은 요청을 몇 번 반복하더라도 결과가 항상 동일하다는 성질이다. 단순히 응답이 같은 것이 아니라, 서버 상태(데이터)까지 첫 요청과 동일해야 진짜 멱등하다고 할 수 있다.
- 결제 승인을 3번 보냈는데, 응답이 다 200이어도, 실제 3번이 결제됐다면 멱등하지 않다.
- 결제 승인을 3번 보냈는데, 실제 결제는 1번만 일어나고 나머지는 첫 응답을 그대로 돌려준다면 멱등하다.
HTTP 명세상 메서드별 멱등성은 GET, PUT, DELETE는 정의상 멱등하다. 하지만 POST는 호출할 때마다 새 리소스가 들기 때문에 멱등하지 않다.
그래서 멱등키가 필요한 곳은 주로 POST의 경우이다. 토스의 경우도 POST에만 멱등키를 받고, 나머지는 자체 HTTP에서 보장하므로 멱등키를 무시한다.
어떤 문제를 해결하려고 나왔을까? 왜 사용 할까?
네트워크는 언제든 끊기게 되고, 클라이언트가 요청을 보냈는데 응답을 못 받으면 두 가지 가능성이 동시에 존재한다.
- 서버가 처리하지 못해서 재시도를 해야함
- 서버가 처리는 했지만, 응답만 유실 → 재시도한다면 이중 처리이다.
응답을 받지 못했다는 사실만으로 1,2인지 클라이언트는 구분하기 어렵다.
그래서 멱등성이 있다면, 1,2이든 신경 쓸 필요 없이 그냥 같은 멱등키로 재시도를 하고, 2번이라면 서버가 첫 결과를 그대로 돌려주고, 1였다면 정상 처리가 된다.
즉, 멱등성은 안심하고 재시도할 수 있는 권리를 제공해준다고 생각한다. 재시도 전략과 멱등성을 같이 생각해야할 것이다.
어떻게 동작하나?
멱등키로 중복 식별
토스의 서버 기준, 멱등 요청의 식별은 다음 조합으로 한다.
멱등키(Idempotency-Key) + API 키 + API URL + HTTP 메서드
- 이 조합이 같다면 중복 요청으로 판단하고 첫 요청과 동일한 응답으로 반환한다.
- API 키, URL, 메서드가 다르다면 같은 멱등키여도 새 요청으로 받아들인다.
토스 블로그 기준 서버의 내부 의사 흐름
- 요청에 멱등키가 있는지 확인
- 멱등키 저장소 DB, Redis를 조회한다.
- 같은 키 기록이 이미 있으면, 저장된 응답을 그대로 반환한다.
- 없다면 실제 처리 후 응답을 멱등키와 함께 저장한다.
- 저장 기간이 지나면 같은 키로 새로운 요청이 가능하도록 한다. (멱등키의 유효기간)
저장소가 단순 K-V이라면 Redis로, 복잡하다면 별도 DB/테이블을 권장한다고 한다.
언제 쓰고, 언제 안 쓰나?
쓸 때:
- 부수 효과가 있고, 중복 실행이 되면 손해가 나는 모든 쓰기 작업
안 쓸 때:
- 부수 효과가 없고 본질적으로 멱등한 조회 작업
- 중복 실행되도 무해하고, 멱등 관리 비용이 더 큰 단발성 로직
재시도 정책: 횟수과 관격, 예외처리

재시도 정책
| 항목 | 권장 | 이유 |
|---|---|---|
| 횟수 | 결제 같은 민감 작업 2~3회, 무한 재시도 금지 | 재시도가 길어지면 사용자 대기·부하 폭증 |
| 간격 | 지수 백오프 (예: 1s → 2s → 4s) | 고정 간격은 회복 안 된 서버를 계속 때림 |
| 지터(jitter) | 백오프에 무작위 흔들기 추가 | 다수 클라이언트 동시 재시도(thundering herd) 방지 |
| 타임아웃 | connection / read 타임아웃 분리 설정 | read 타임아웃 없으면 영원히 매달림 |
| 전체 데드라인 | 누적 재시도 시간 상한 설정 | 총 N초 안에 못 하면 포기 예산 관리 |
예외처리
재시도를 해야하는것 - 일시적이고 결과가 불확실한 상황
- 연결 거부, 리셋, read 타임 아웃
SocketTimeoutException,ResourceAccessException - 5xx 에러
- 409
IDEMPOTENT_REQUEST_PROCESSING첫 요청이 아직 처리중
재시도 안해도 되는것 - 영구적으로 결과가 정해진 상황
- 4xx 에러
- 비즈니스의 거절: 잔액 부족, 카드 한도 초과, 한도 정책 위반
주의 사항
멱등 요청이 에러 났을 때, 멱등키를 바꿔서 다시 보내면 안됨 첫 요청이 사실 서버에서 처리됐을 수도 있는데, 새 키로 보내게 된다면 이를 새 요청으로 보고 또 처리를 하게 된다. 즉, 이중 결제가 된다. 그래서 에러가 나더라도 같은 키를 유지하거나, 불확실하다면 결제 조회 API로 실제 상태를 확인하도록 한다.
Spring에서 재시도 구현
- 멱등키는 요청 진입점에서 1회 생성해서 변수/DB에 보관하고, HTTP 클라이언트 호출과 재시도에서 동일한 키를 사용한다.
- 재시도는 직접 루프보다
Resilience4j Retry또는Spring Retry로 분리한다.- @Retryable에서 retryOn / retryExceptions에 재시도가 가능한 예외만 등록하고, 비즈니스 거절을 noRetryOn으로 처리한다.
- 백오프 설정에
multiplier 지수와randomized 지터를 적용시킨다.
- HTTP 상태별로 분리를 처리한다.
- 5xx/타임아웃은 재시도로 신호로 변환하고,
- 4xx 에러는 즉시 실패 예외로 변환하는 에러 핸들러를 둔다.
남에게 설명한다면 어떻게 설명할 것인가?
엘리베이터 버튼"으로 설명한다.
엘리베이터 버튼을 한 번 누르나 열 번 누르나 엘리베이터는 한 번만 온다. 이미 불이 켜진 버튼은 더 눌러도 아무 일이 안 일어난다. 멱등성이 바로 이 버튼의 성질이다.
그런데 만약 누를 때마다 엘리베이터가 한 대씩 따로 출동한다면(=비멱등), 무서워서 다시 못 누른다. 멱등성은 “불안하면 안심하고 다시 눌러도 된다"는 보장이고, 재시도는 그 보장을 믿고 다시 누르는 행위다.
멱등키는 “이 버튼 누름은 아까 그 누름과 같은 건지"를 식별하는 표식이다. 그래서 재시도할 때 표식(키)을 바꾸면, 서버 입장에선 “다른 버튼"이 되어 진짜로 한 번 더 출동한다.
추가 궁금한 질문들
- 왜 멱등키를 UUID v4 같은 무작위 값으로 만들라고 할까? 주문번호·시퀀스를 키로 쓰면 뭐가 문제일까? (예측 가능성·충돌·재사용 위험 관점)
- 멱등키 저장소를 자체 구현한다면 TTL은 토스의 15일에 맞춰야 하나, 비즈니스 요건에 맞춰야 하나? 저장 시점은 “요청 받자마자 vs 처리 성공 후”?
- “요청 처리 중(409)” 상태를 직접 구현한다면 동시성을 어떻게 잡나? (DB 유니크 제약 + 상태 컬럼 vs Redis 분산 락 — Level 3에서 본 락 경합 주제와 연결)
- 재시도 + 멱등키 컴포넌트화: 여러 외부 API에 공통으로 쓸 재시도/멱등 처리 로직을 어떻게 추상화할까? (AOP, 데코레이터, Resilience4j 조합)
- 결제 승인은 멱등키가 없어도 안전한가? 토스 결제 승인은 paymentKey가 이미 1회성이라 사실상 중복 승인이 막히는데, 그럼 멱등키의 추가 가치는 어디서 나오나? (네트워크 유실 시 동일 응답 회수 관점)
- 테스트 전략: 멱등성/재시도를 어떻게 검증할까? 응답 유실 상황을 어떻게 재현하고, 동일 키 중복 호출 시 부수효과가 1회인지 어떻게 단언할까?
참고 (토스페이먼츠 공식 문서)
- 멱등키 헤더 / 인증 헤더 설정 —
docs.tosspayments.com/reference/using-api/idempotency-key - 멱등성이 뭔가요? (블로그) —
docs.tosspayments.com/blog/what-is-idempotency - 코어 API (결제 승인·취소) —
docs.tosspayments.com/reference