클라이언트는 실패를 구분할 수 없다

요청을 보냈는데 응답이 오지 않았다. 두 경우 중 하나다.

  1. 서버에 도달하지 못했다 → 아무 일도 일어나지 않았다
  2. 서버는 처리했는데 응답이 유실됐다 → 이미 처리됐다

클라이언트 입장에서 이 둘은 구분되지 않는다. 그래서 재시도할 수밖에 없고, 재시도가 안전하려면 연산이 멱등해야 한다.

멱등하다는 것

여러 번 실행해도 결과가 한 번 실행한 것과 같다는 뜻이다.

결제, 주문 생성, 포인트 적립처럼 중요한 연산이 대부분 POST다.

Idempotency-Key

해법은 클라이언트가 요청마다 고유한 키를 만들어 보내는 것이다.

POST /payments
Idempotency-Key: 9f2c1b7e-...

서버는 이렇게 처리한다.

  1. 키가 이미 있으면 → 저장해둔 첫 결과를 그대로 반환한다
  2. 없으면 → 처리하고, 키와 결과를 함께 저장한다

핵심은 두 번째 요청에 오류를 주지 않는다는 점이다. 클라이언트는 재시도의 결과로 정상 응답을 받고, 서버에는 결제가 한 번만 남는다.

놓치기 쉬운 것들

동시성. 재시도가 첫 요청과 겹칠 수 있다. 키 컬럼에 유니크 제약을 걸어 DB가 중복을 막게 한다. 애플리케이션에서 “조회 후 없으면 삽입”으로 처리하면 그 사이에 끼어들 틈이 생긴다.

키를 언제 저장하느냐. 처리를 끝내고 저장하면 그 사이의 재시도가 중복 처리된다. 키 등록과 실제 처리를 같은 트랜잭션에 두어야 한다.

키의 수명. 영원히 보관할 수는 없다. 재시도가 현실적으로 발생할 기간(보통 24시간~며칠)만 두고 정리한다.

키를 누가 만드나. 클라이언트다. 서버가 만들면 요청마다 달라져서 의미가 없다.

정리

재시도는 선택이 아니라 분산 환경의 기본값이다. 재시도해도 안전한 API를 먼저 만들고 나서 재시도 정책을 논하는 순서가 맞다.