글 / 시스템 연계

멱등 키와 중복 요청 처리

요청마다 고유 키를 부여하고 키와 결과를 한 트랜잭션에 저장하면, 재시도로 두 번 수신된 요청도 한 번만 처리된다.

작성일 · 읽는 시간 2분 · PostgreSQL 18 기준

중복 요청의 발생 원인

응답을 수신하지 못한 측은 요청이 처리되었는지 알 수 없다. 처리는 완료되었으나 응답만 손실되었을 수도 있다. 이때 요청을 재전송하면 수신 측은 같은 주문을 두 번 생성한다.

요청별 고유 키 부여

멱등 키(idempotency key)는 송신 측이 요청 하나에 부여하는 고유 값이다. 재시도할 때는 키를 새로 생성하지 않고 최초의 키를 그대로 전송한다. 수신 측은 키와 처리 결과를 함께 저장하고, 같은 키가 다시 수신되면 저장한 결과를 반환한다.

응답 손실 후 같은 멱등 키로 재전송된 요청의 처리 순서

sql
CREATE TABLE idempotency_keys (
    idem_key     text PRIMARY KEY,
    request_hash text NOT NULL,
    response     jsonb,
    created_at   timestamptz NOT NULL DEFAULT now()
);

유니크 제약을 이용한 경쟁 상태 방지

키의 존재 여부를 조회하고 없으면 처리하는 순서에서는, 같은 키가 동시에 두 번 수신되면 두 요청이 모두 통과한다. 따라서 조회하지 않고 키부터 삽입하여 판단을 유니크 제약에 위임한다.

sql
BEGIN;

-- 이미 있는 키면 행이 돌아오지 않는다
INSERT INTO idempotency_keys (idem_key, request_hash)
VALUES ('7f3a9c2e', 'b1946ac9')
ON CONFLICT (idem_key) DO NOTHING
RETURNING idem_key;

-- 행이 돌아왔을 때만 주문을 만들고 응답을 남긴다
INSERT INTO orders (item, qty) VALUES ('A-100', 2);
UPDATE idempotency_keys SET response = '{"order_id": 1001}'
WHERE idem_key = '7f3a9c2e';

COMMIT;

행이 반환되지 않으면 주문을 생성하지 않고 저장된 response를 읽어 반환한다. 먼저 수신된 요청의 트랜잭션이 종료되지 않았다면 나중에 실행된 INSERT는 그 트랜잭션이 종료될 때까지 대기한다. 키와 주문이 한 트랜잭션에 속하므로 처리가 실패하면 키도 함께 롤백되고, 다음 재시도는 새로 처리된다.

같은 키의 두 요청이 동시에 수신될 때 두 트랜잭션의 진행 순서

참고

대기 후의 조회가 방금 커밋된 response를 읽으려면 격리 수준이 PostgreSQL 기본값인 Read Committed여야 한다.

추가 결정 사항

  • request_hash: 키는 같으나 본문이 다르면 재시도가 아니므로 거절한다.
  • 보관 기간: 재시도가 수신될 수 있는 기간에만 보관하고 created_at을 기준으로 삭제한다.