클라이언트가 주문 생성 요청을 보냈는데 응답을 받기 전에 연결이 끊겼다고 하자. 서버가 처리하지 않았을 수도 있고 처리는 끝났지만 응답만 사라졌을 수도 있다. 클라이언트는 실패인지 성공인지 알 수 없다.
이때 재시도를 막으면 실제로 처리되지 않은 주문을 잃고 그대로 다시 보내면 주문이 두 개 생길 수 있다. 멱등성(idempotency: 같은 작업을 한 번 실행하든 여러 번 실행하든 최종 상태가 같게 되는 성질)은 이 불확실한 재시도를 안전한 상태 전이로 바꾸는 규칙이다.
같은 요청과 같은 결과를 먼저 정의해야 한다
사용자 7의 이메일을 x@example.com으로 설정은 여러 번 실행해도 최종 이메일이 같다. 반면 잔액에 10,000원을 더한다는 실행할 때마다 값이 바뀐다.
설정: email = x@example.com
한 번 → x@example.com
세 번 → x@example.com
증가: balance = balance + 10000
한 번 → +10000
세 번 → +30000
HTTP PUT이 멱등하고 POST가 멱등하지 않다는 설명만으로 업무 안전성을 판단하면 부족하다. 메서드의 일반적 의미와 실제 서버 구현은 다를 수 있다. 어떤 자원을 어떤 식별자로 바꾸며 중복 실행 때 무엇을 반환할지 업무 단위로 정의해야 한다.
멱등성 키는 재시도된 같은 작업을 알아본다
멱등성 키(idempotency key: 클라이언트가 한 논리적 작업의 모든 재시도에 동일하게 보내는 고유 식별자)를 사용하면 서버는 이전 처리 여부를 찾을 수 있다.
첫 요청
Idempotency-Key: order-create-8f3a
body: {item: 42, quantity: 1}
재시도
Idempotency-Key: order-create-8f3a
body: {item: 42, quantity: 1}
서버는 키가 처음이면 작업을 처리하고 결과를 저장한다. 같은 키가 다시 오면 새 주문을 만들지 않고 저장된 결과를 돌려준다.
키는 사용자나 계정 범위와 함께 저장해야 한다. 서로 다른 사용자가 우연히 같은 문자열을 사용해도 한 사람의 결과가 다른 사람에게 반환되면 안 된다.
키 확인과 업무 변경은 하나의 원자적 경계에 있어야 한다
다음 구현은 경쟁 조건이 있다.
1. 멱등성 키가 없는지 조회
2. 주문 생성
3. 멱등성 키 저장
같은 키의 요청 두 개가 동시에 1번을 통과하면 주문을 두 개 만들 수 있다. 키 선점과 업무 변경, 결과 저장 사이의 실패도 고려해야 한다.
데이터베이스의 고유 제약(unique constraint: 같은 범위에서 특정 키 값이 중복 저장되지 않게 하는 규칙)과 트랜잭션을 이용해 한 요청만 키를 선점하게 만들 수 있다. 업무 변경과 멱등성 상태를 같은 트랜잭션에 묶을 수 있다면 경계가 명확해진다.
트랜잭션 시작
멱등성 키 행 삽입 ─ 중복이면 기존 상태 확인
주문 생성
응답에 필요한 결과 저장
커밋
외부 결제처럼 같은 트랜잭션에 넣을 수 없는 작업은 외부 시스템이 제공하는 멱등성 키를 함께 사용하거나 상태 머신과 재조정 작업을 설계해야 한다.
진행 중인 요청과 완료된 요청을 구분한다
같은 키가 저장되어 있다는 사실만으로 성공 결과가 준비됐다고 단정할 수 없다. 첫 요청이 아직 실행 중이거나 중간에 실패했을 수 있다.
IN_PROGRESS → SUCCEEDED
└→ FAILED_RETRYABLE
└→ FAILED_FINAL
두 번째 요청이 IN_PROGRESS를 만나면 잠시 기다리거나 진행 중 응답을 돌려줄 수 있다. 실패 상태에서 같은 작업을 이어 갈지 새 키를 요구할지도 정해야 한다.
상태와 응답 코드, 응답 본문을 저장하면 재시도에 처음과 일관된 결과를 돌려줄 수 있다. 다만 개인정보나 큰 응답 전체를 무조건 저장하기보다 다시 조회할 자원 식별자를 남기는 방법도 있다.
같은 키에 다른 요청이 오면 거절해야 한다
클라이언트 실수로 같은 멱등성 키를 다른 본문에 재사용할 수 있다.
키 K, 첫 요청 : item 42, quantity 1
키 K, 다음 요청: item 99, quantity 3
키만 보고 첫 결과를 돌려주면 클라이언트는 item 99 주문이 성공했다고 오해할 수 있다. 서버는 중요한 요청 필드의 해시나 정규화된 요청 표현을 키와 함께 저장해, 같은 키의 의미가 바뀌면 오류로 알려야 한다.
해시(hash: 입력 데이터를 고정 길이 값으로 바꾸어 같은 입력인지 빠르게 비교하게 하는 함수 결과)는 요청의 비밀을 보호하는 암호화가 아니다. 충돌 가능성과 민감 정보 저장 여부를 고려하고 업무 비교에 필요한 필드를 명확히 정한다.
멱등성 기록도 보관 기한이 필요하다
모든 키와 결과를 영원히 보관할 수는 없다. 키 TTL(Time To Live: 멱등성 기록을 재시도 판정에 사용할 보관 시간)을 정하면 그 기간 안의 재시도만 같은 작업으로 인식한다.
보관 기간은 클라이언트의 최대 재시도 시간, 네트워크 지연, 업무 중복의 피해를 보고 정한다. TTL이 끝난 뒤 같은 키가 다시 오면 새 작업으로 처리될 수 있다는 계약도 알려야 한다.
키 정리와 업무 데이터 보존은 다른 정책이다. 멱등성 행을 지운다고 생성된 주문까지 지우는 것은 아니다.
정확히 한 번 전달보다 효과를 한 번 만드는 데 집중한다
네트워크와 메시지 큐에서는 요청이 손실되거나 중복될 수 있다. 모든 계층이 “정확히 한 번” 전달을 보장한다고 기대하기보다, 적어도 한 번(at-least-once: 손실을 피하려고 중복 가능성을 허용하는 전달 방식) 도착해도 업무 효과가 한 번만 생기게 만드는 편이 현실적이다.
멱등성은 모든 부수 효과를 자동으로 묶지 않는다. 주문은 하나지만 이메일을 두 번 보낼 수 있다. 데이터베이스 변경, 메시지 발행, 외부 호출마다 같은 논리적 작업 식별자를 전달하고 중복 처리 규칙을 연결해야 한다.
이해 확인 질문과 답변
같은 HTTP 메서드면 멱등성도 항상 같을까?
아니다. 메서드의 의미는 설계 기준을 주지만 실제 최종 상태는 서버의 업무 구현이 결정한다. PUT도 호출마다 부수 효과를 추가하면 멱등하지 않을 수 있고 POST도 멱등성 키를 사용해 안전하게 만들 수 있다.
멱등성 키가 있으면 중복 요청 경쟁이 자동으로 사라질까?
아니다. 키 확인과 키 선점, 업무 변경이 원자적으로 연결되어야 한다. 별도 단계로 두면 동시에 들어온 요청이 모두 “키 없음”을 보고 실행할 수 있다.
같은 키로 다른 요청 본문이 오면 어떻게 해야 할까?
처음 요청과 의미가 달라졌음을 오류로 알려야 한다. 중요한 입력의 비교값을 키와 함께 저장해 조용히 이전 결과를 돌려주는 일을 막는다.
멱등성 키를 영원히 저장해야 할까?
반드시 그렇지는 않다. 재시도 가능 기간과 중복 피해를 기준으로 보관 기한을 정한다. 기한이 지난 키를 어떻게 취급하는지도 API 계약에 포함한다.
AI에게 이렇게 요청할 수 있다
주문 생성 요청의 응답이 유실되어 같은 Idempotency-Key로 두 요청이 동시에 들어오는 시간표를 보여 줘. 고유 제약과 트랜잭션으로 한 요청만 처리하고, IN_PROGRESS와 SUCCEEDED 결과를 재사용하며, 같은 키에 다른 본문이 오면 거절하는 규칙을 설명해 줘.
멱등성은 중복 전달 자체를 없애는 보장이 아니다. 같은 논리적 작업을 식별하고, 상태 변경과 결과 저장을 하나의 경계로 묶어 중복 효과를 막는 설계다.
'배움과 성장 > 백엔드·데이터' 카테고리의 다른 글
| 여러 서버는 누가 리더인지 어떻게 합의할까? 정족수와 로그 복제 (0) | 2026.09.22 |
|---|---|
| LSM 트리는 쓰기를 어떻게 모아 디스크에 정리할까? SSTable과 컴팩션 (0) | 2026.09.19 |
| MVCC는 읽기와 쓰기가 서로 기다리지 않게 어떻게 도울까? 행 버전과 스냅샷 (0) | 2026.09.18 |
| WAL은 데이터 페이지보다 로그를 왜 먼저 쓸까? 변경 기록과 체크포인트 (0) | 2026.09.17 |
| 여러 복제본은 언제 같은 값을 보여야 할까? 복제와 일관성 (0) | 2026.09.09 |
댓글