멱등성(idempotency)은 같은 연산을 한 번 하든 여러 번 하든 결과가 같다는 성질이다. 네트워크는 실패하고 메시지는 중복되므로, 재시도가 안전하려면 받는 쪽이 멱등해야 한다.
HTTP 메서드
- GET, PUT, DELETE는 명세상 멱등이다. 같은 PUT을 두 번 보내도 리소스는 같은 상태가 된다
- POST는 멱등이 아니다. 결제 요청을 두 번 보내면 두 번 결제될 수 있다
- 그래서 결제 같은 POST에는 클라이언트가 만든 멱등 키(Idempotency-Key)를 헤더로 보내게 하고, 서버는 같은 키의 두 번째 요청에 첫 결과를 그대로 돌려준다
멱등 키를 서버에서 처리하기
헤더만 보낸다고 효과가 생기지 않는다. 서버가 키를 저장하고 비교하는 코드를 직접 갖춰야 한다.
키 + 요청자 + 요청 본문 해시 + 상태(processing/completed) + 저장한 응답 + 만료 시각- 키를 원자적으로 등록한다. 두 요청이 동시에 "없음"을 확인하고 둘 다 결제하면 안 되므로, 유니크 제약에
INSERT ... ON CONFLICT DO NOTHING이나 RedisSET key value NX로 한 요청만 등록에 성공하게 한다 - 등록에 성공한 요청만 실제 작업을 하고, 끝나면 상태 코드와 응답 본문을 함께 저장해
completed로 바꾼다 - 같은 키가 다시 오면 작업하지 않고 저장한 응답을 돌려준다
IETF 초안(2026 기준 만료된 초안 07)이 권하는 오류 응답은 이렇다.
- 키가 필요한 API에 키가 없으면
400 - 같은 키를 다른 본문으로 다시 쓰면
422. 키 하나가 서로 다른 작업을 가리키면 안 되므로 본문 해시(지문, fingerprint)를 함께 저장해 비교한다 - 첫 요청이 아직 처리 중인데 재시도가 오면
409
구현할 때 함께 정할 것:
- 키 범위: 키만으로 조회하면 남의 키로 남의 결과를 꺼낼 수 있다. 초안도 요청자 정보와 묶은 복합 키를 권한다
- 새 작업은 새 키, 재시도는 같은 키: 네트워크 재시도는 같은 키를 다시 보내고, 사용자가 결제를 새로 누르면 새 키를 만든다. 보통
crypto.randomUUID()같은 UUID를 쓴다 - 만료: 키를 영원히 둘 필요는 없지만, 재시도가 늦게 올 수 있는 작업은 충분히 오래 보관한다. 만료 정책은 문서로 공개한다
- 멱등 키는 같은 키의 재시도를 한 작업으로 묶을 뿐, 외부 결제사·DB·큐까지 포함한 전체에서 정확히 한 번(exactly-once) 실행을 보장하지는 않는다. 각 단계에도 중복 방지가 필요하다
메시지 소비자
Kafka 같은 브로커와 아웃박스 워커는 보통 at-least-once(최소 한 번 전달)로 전달한다. "최소 한 번"이라 같은 이벤트가 두 번 올 수 있다.
- 같은 ID로 덮어쓰기: 검색 색인이나 캐시에 문서 ID 기준으로 upsert하면 중복이 와도 결과가 같다
- 처리한 이벤트 ID 기록: 처리 테이블에 이벤트 ID를 유니크 키로 남기고, 이미 있으면 건너뛴다
- 버전 비교(낙관적 잠금과 같은 발상): 이벤트에 버전·타임스탬프를 담아 현재보다 오래된 변경은 무시한다. 순서 꼬임도 함께 막는다
INSERT INTO processed_events (event_id) VALUES ($1)
ON CONFLICT (event_id) DO NOTHING; -- 영향받은 행이 0이면 이미 처리됨혼동 주의
멱등은 "부수 효과(side effect)가 없다"와 다르다. DELETE는 상태를 바꾸지만 여러 번 해도 결과가 같아 멱등이다. 응답 코드는 달라질 수 있다(첫 번째 204, 두 번째 404).
이벤트가 늦게·중복으로 도착하는 세계에서 결국 같은 상태로 모이게 하는 장치라 최종 일관성과 짝을 이룬다. 요청 횟수 자체를 제한하는 것은 요청 제한 (Throttling), 재사용 가능한 순수 함수의 성질은 명령-질의 분리과도 이어진다.
출처: RFC 9110: Idempotent Methods · IETF 초안: The Idempotency-Key HTTP Header Field · 초안 07 §2.7 — Error Handling · 초안 07 §5 — Security Considerations · Redis — SET (NX) · microservices.io: Idempotent Consumer