백엔드
웹 API
API 멱등성 설계: 멱등 키, 원자적 저장, 보관 기간

API 멱등성 설계: 멱등 키, 원자적 저장, 보관 기간

들어가며

결제 버튼을 눌렀는데 화면이 멈춰서 한 번 더 눌렀더니 결제가 두 건 잡혔다는 이야기는 흔합니다. 서버 입장에서는 첫 요청을 이미 처리했지만 응답이 클라이언트에 닿기 전에 연결이 끊겼고, 클라이언트는 실패로 알고 다시 보낸 것입니다.

네트워크는 언제든 끊기기 때문에 재시도는 피할 수 없습니다. 이번 글에서는 재시도가 와도 결과가 한 번만 반영되게 만드는 멱등성을 정리하고, 멱등 키를 서버에서 어떻게 다루는지까지 설명합니다.


1. 멱등성이란

멱등성(idempotency) 은 같은 작업을 여러 번 해도 한 번 한 것과 서버 상태가 같은 성질입니다. 여기서 기준은 응답이 아니라 서버 상태입니다.

DELETE /orders/7 을 두 번 보내면 첫 번째는 204, 두 번째는 404 가 올 수 있습니다. 응답은 달라도 주문 7이 없는 상태는 같으므로 DELETE 는 멱등입니다. 반대로 count = count + 1 을 두 번 실행하면 상태가 매번 달라지므로 멱등이 아닙니다.

HTTP 명세(RFC 9110)는 메서드마다 멱등 여부를 정해 두었습니다.

안전은 메뉴판만 보는 것, 멱등은 같은 주문을 몇 번 말해도 한 접시만 나오는 것이에요.

메서드안전멱등
GET조회
HEAD헤더만 조회
PUT전체 교체
DELETE삭제두 번째는 404 여도 됨
PATCH일부 수정내용에 따라 다름
POST생성·실행멱등 키로 보완
보장 보장 안 됨 요청 내용에 따라 다름
안전한 메서드는 모두 멱등이지만 멱등이라고 안전한 것은 아니다. 재시도를 자동으로 걸 수 있는 것은 멱등 메서드뿐이고, POST 는 멱등 키가 있어야 한다.

GET·HEAD 는 상태를 바꾸지 않는 안전한 메서드라 당연히 멱등입니다. PUT 은 자원을 통째로 같은 값으로 바꾸니 몇 번 보내도 결과가 같고, DELETE 도 앞의 이유로 멱등입니다. POST 는 보낼 때마다 새 자원을 만들 수 있어 멱등이 아닙니다. 별도 명세(RFC 5789)로 정의된 PATCH 는 멱등이 보장되지 않고 내용에 따라 다릅니다. "값을 3으로" 는 멱등이지만 "값에 1을 더하기" 는 아닙니다.

2. 멱등 키로 POST 를 안전하게

결제나 주문 생성처럼 POST 로 해야 하는 작업은 멱등 키(Idempotency-Key) 로 멱등성을 만듭니다. 클라이언트가 요청마다 UUID 같은 고유 값을 만들어 헤더에 붙이고, 재시도할 때는 같은 값을 다시 씁니다.

POST /payments HTTP/1.1
Idempotency-Key: 9f1c2e7a-4b3d-4c11-8e2a-5d6f7a8b9c0d
Content-Type: application/json

{"order_id": 7, "amount": 30000}

서버는 키와 처리 결과를 저장해 두고, 같은 키가 다시 오면 작업을 다시 하지 않고 저장해 둔 응답을 돌려줍니다.

멱등 키는 주문서에 적힌 주문 번호예요. 같은 번호가 또 오면 새로 만들지 않고 영수증을 다시 줘요.

📱클라이언트키 생성·보관🖥️서버결제 처리🗄️키 저장소Redis · DB① POST 키 9f1c② SET NX 성공결제 1건 처리③ 결과 저장④ 201 (유실)⑤ 재시도 키 9f1c⑥ SET NX 실패⑦ 저장된 201
첫 응답이 사라져도 같은 키로 다시 보내면 저장된 결과를 받는다. 대신 서버는 키와 결과를 보관할 저장소와 그 비용을 떠안는다.

첫 응답이 중간에 사라져도 두 번째 요청은 저장된 결과를 받으므로 결제는 한 건만 남습니다. 이 헤더는 IETF 에서 표준 초안으로 정리되고 있고, 여러 결제 API 가 같은 이름으로 쓰고 있습니다.

3. 키 저장은 원자적으로

"키가 있는지 확인하고, 없으면 저장한다" 를 두 번의 호출로 나누면 문제가 생깁니다. 같은 키로 요청 둘이 거의 동시에 오면 둘 다 "없음" 을 보고 둘 다 처리하기 때문입니다.

확인과 저장은 한 번의 원자적 연산으로 해야 합니다. Redis 라면 SET NX, DB 라면 유일 제약과 ON CONFLICT 가 그 역할을 합니다.

# 키가 없을 때만 저장(NX). 성공한 요청 하나만 처리를 시작한다
SET idem:9f1c2e7a PENDING NX EX 86400
# 처리가 끝나면 결과를 덮어쓴다
SET idem:9f1c2e7a '{"status":201,"payment_id":"p_31"}' EX 86400
INSERT INTO idempotency_keys (key, request_hash, status)
VALUES ('9f1c2e7a', 'a41f...', 'PENDING')
ON CONFLICT (key) DO NOTHING;   -- 0행이면 이미 누가 처리 중이거나 끝냄

처리 중인 키로 요청이 또 오면 409 Conflict 로 거절하거나, 끝날 때까지 기다렸다가 같은 결과를 돌려줍니다. 같은 키인데 본문이 다르면 키를 잘못 재사용한 것이므로 처리하지 않고 오류로 답합니다. 그래서 키와 함께 요청 본문의 해시를 저장해 비교하는 경우가 많습니다.

4. 기록은 얼마나 오래 두나

멱등 기록에는 TTL(Time To Live, 보관 기간)을 둡니다. TTL 이 지나면 같은 키도 새 요청으로 처리되므로, 보관 기간은 재시도가 올 수 있는 기간보다 길어야 합니다.

흔히 놓치는 것은 기간을 어디서부터 세느냐입니다. 작업이 큐에서 오래 기다리는 시스템이라면, 접수 시점부터 센 TTL 이 작업이 끝나기도 전에 만료될 수 있습니다.

멱등 기록은 주문 번호를 적어 둔 장부예요. 요리가 나오기도 전에 장부를 버리면 같은 주문을 또 받아요.

접수부터 24시간중복 처리
작업큐 대기처리기록TTL 24h만료재시도새 요청 취급01224364860시간

기록이 먼저 사라져, 40시간 뒤의 재시도가 새 요청으로 다시 처리된다.

끝난 뒤 24시간한 번만
작업큐 대기처리기록처리 중TTL 24hTTL 시작재시도저장된 결과01224364860시간

작업이 끝난 시점부터 세므로 같은 재시도가 저장된 결과를 받는다.

기록 TTL 을 접수부터 세면 대기가 길어질 때 처리 전에 만료된다. 끝난 시점부터 세면 대기 길이와 무관해지지만, 접수 때 걸어 둘 안전망 TTL 은 따로 필요하다(시간은 어림).

접수할 때는 넉넉한 안전망만 걸어 두고, 작업이 끝나는 시점에 정식 TTL 을 다시 거는 편이 안전합니다. 이때 정상 완료뿐 아니라 실패·취소처럼 작업이 끝나는 모든 경로에서 TTL 을 걸어야 기록이 남거나 일찍 사라지지 않습니다.

헷갈리기 쉬운 점

  • 두 번째 DELETE 가 404 라서 멱등이 아니라는 오해. 멱등은 응답이 아니라 서버 상태로 판단합니다.
  • 멱등 기록은 짧게 두면 된다는 오해. 큐 대기를 포함한 작업 수명보다 짧으면, 만료 뒤의 재시도가 조용히 중복 처리됩니다.
  • 멱등 키를 서버가 만든다는 오해. 재시도할 때 같은 값을 다시 보내야 하므로 키는 클라이언트가 만들고 보관합니다.

면접에서 자주 받는 질문

처리 중에 같은 키로 요청이 또 오면 어떻게 하나요?

키를 원자적으로 저장해 두면 두 번째 요청은 "처리 중" 상태를 보게 됩니다. 이때 409 Conflict 로 거절하거나, 처리가 끝날 때까지 기다린 뒤 저장된 결과를 돌려줍니다. 확인과 저장을 나눠 하면 두 요청이 모두 통과하므로 반드시 한 번의 연산으로 합니다.

같은 키인데 요청 본문이 다르면요?

클라이언트가 키를 잘못 재사용한 것이므로 처리하지 않고 오류로 답합니다. 이를 가려내려고 키와 함께 본문 해시를 저장해 두고, 다시 온 요청의 해시와 비교합니다.

at-least-once 메시지 큐와 멱등성은 어떤 관계인가요?

at-least-once 는 메시지가 최소 한 번 전달된다는 보장이라, 같은 메시지가 두 번 올 수 있습니다. 그래서 소비자가 메시지 ID 로 처리 이력을 남기는 식으로 멱등하게 만들어야 중복 처리가 생기지 않습니다.

정리

  • 멱등성은 여러 번 해도 서버 상태가 한 번 한 것과 같은 성질이며, 응답이 아니라 상태로 판단합니다.
  • POST 는 클라이언트가 만든 멱등 키와 서버의 처리 기록으로 멱등하게 만들고, 키 저장은 원자적으로 합니다.
  • 기록의 TTL 은 재시도가 올 수 있는 기간보다 길어야 하고, 접수가 아니라 작업이 끝난 시점부터 세는 편이 안전합니다.

두 번 눌러도 한 번 누른 것처럼 동작하게 만드는 일, 그리고 그 기록은 작업이 끝난 날부터 센다는 것이 멱등성 설계의 핵심입니다.