클라이언트는 실패를 구분할 수 없다
요청을 보냈는데 응답이 오지 않았다. 두 경우 중 하나다.
- 서버에 도달하지 못했다 → 아무 일도 일어나지 않았다
- 서버는 처리했는데 응답이 유실됐다 → 이미 처리됐다
클라이언트 입장에서 이 둘은 구분되지 않는다. 그래서 재시도할 수밖에 없고, 재시도가 안전하려면 연산이 멱등해야 한다.
멱등하다는 것
여러 번 실행해도 결과가 한 번 실행한 것과 같다는 뜻이다.
GET— 읽기만 하니 멱등PUT— “이 값으로 만들어라”이므로 멱등DELETE— 이미 지워졌으면 지워진 상태 그대로. 멱등POST— “새로 만들어라”이므로 멱등하지 않다
결제, 주문 생성, 포인트 적립처럼 중요한 연산이 대부분 POST다.
Idempotency-Key
해법은 클라이언트가 요청마다 고유한 키를 만들어 보내는 것이다.
POST /payments
Idempotency-Key: 9f2c1b7e-...
서버는 이렇게 처리한다.
- 키가 이미 있으면 → 저장해둔 첫 결과를 그대로 반환한다
- 없으면 → 처리하고, 키와 결과를 함께 저장한다
핵심은 두 번째 요청에 오류를 주지 않는다는 점이다. 클라이언트는 재시도의 결과로 정상 응답을 받고, 서버에는 결제가 한 번만 남는다.
놓치기 쉬운 것들
동시성. 재시도가 첫 요청과 겹칠 수 있다. 키 컬럼에 유니크 제약을 걸어 DB가 중복을 막게 한다. 애플리케이션에서 “조회 후 없으면 삽입”으로 처리하면 그 사이에 끼어들 틈이 생긴다.
키를 언제 저장하느냐. 처리를 끝내고 저장하면 그 사이의 재시도가 중복 처리된다. 키 등록과 실제 처리를 같은 트랜잭션에 두어야 한다.
키의 수명. 영원히 보관할 수는 없다. 재시도가 현실적으로 발생할 기간(보통 24시간~며칠)만 두고 정리한다.
키를 누가 만드나. 클라이언트다. 서버가 만들면 요청마다 달라져서 의미가 없다.
정리
재시도는 선택이 아니라 분산 환경의 기본값이다. 재시도해도 안전한 API를 먼저 만들고 나서 재시도 정책을 논하는 순서가 맞다.