멱등성
멱등성(idempotency)은 같은 작업을 반복해 요청해도, 그 작업으로 생기는 효과가 한 번 적용했을 때와 같도록 하는 성질이다. 주문 생성 요청을 보낸 뒤 응답이 끊겼다면 주문이 없는지, 주문은 생겼는데 응답만 잃었는지 호출자는 알기 어렵다. 이때 같은 주문 요청임을 식별하고 이미 저장한 결과를 돌려주면, 호출자는 주문을 하나 더 만들지 않고 결과를 확인할 수 있다.
요청 본문이 같다는 것과 같은 작업이라는 것은 구별해야 한다. 같은 상품을 두 번 주문하려는 사용자도 있으니, 아래 예제에서는 인증을 마친 사용자 식별자와 호출자가 정한 요청 키를 함께 사용한다. 키를 다른 내용에 재사용하면 오류로 처리한다. AWS의 멱등 API 설계

그림은 아래 create_order와 db의 처리 순서다. 주문만 저장되거나 요청 기록만 저장되는 중간 상태가 남지 않도록 두 쓰기를 한 트랜잭션에 넣는다.
주문 하나를 만들고 같은 결과를 다시 받기
Python 3.12와 표준 라이브러리 SQLite를 사용하는 단독 실행 예제다. owner는 앞단에서 인증·권한 확인을 마친 식별자이고, item이 이 예제의 요청 내용 전체다. 메모리 DB의 한 연결에서 처리하므로 실행을 끝내면 기록도 사라진다.
import sqlite3
db = sqlite3.connect(":memory:", isolation_level=None)
db.executescript("""
CREATE TABLE orders(id INTEGER PRIMARY KEY, item TEXT NOT NULL);
CREATE TABLE requests(
owner TEXT NOT NULL, request_key TEXT NOT NULL,
item TEXT NOT NULL, order_id INTEGER NOT NULL,
PRIMARY KEY(owner, request_key)
);
""")
def create_order(owner, request_key, item):
db.execute("BEGIN IMMEDIATE")
try:
row = db.execute(
"SELECT item, order_id FROM requests WHERE owner=? AND request_key=?",
(owner, request_key),
).fetchone()
if row is not None:
if row[0] != item:
raise ValueError("KEY_REUSED_WITH_DIFFERENT_REQUEST")
order_id = row[1]
else:
order_id = db.execute(
"INSERT INTO orders(item) VALUES (?)", (item,)
).lastrowid
db.execute(
"INSERT INTO requests VALUES (?, ?, ?, ?)",
(owner, request_key, item, order_id),
)
db.commit()
return order_id
except Exception:
db.rollback()
raise
if __name__ == "__main__":
first = create_order("alice", "request-1", "book")
again = create_order("alice", "request-1", "book")
assert first == again
assert db.execute("SELECT COUNT(*) FROM orders").fetchone()[0] == 1
print(first, again) # 1 1
db.close()처음 호출은 주문과 요청 기록을 저장하고, 다음 호출은 같은 키의 order_id를 읽는다. 같은 키에 pen을 보내면 ValueError가 발생하고 트랜잭션은 롤백된다. 다른 사용자의 같은 키는 복합 기본 키에서 별개의 작업으로 취급한다.
except는 트랜잭션 정리를 맡고 원래 예외를 다시 전달한다. 자동 재시도나 기본 결과를 만드는 분기는 넣지 않았다. BEGIN IMMEDIATE와 복합 기본 키는 이 SQLite 예제의 쓰기 순서와 요청 식별을 지키는 데 쓰인다. 연결을 여러 개 쓰는 서비스로 옮길 때도 중복 판정과 효과 기록이 같은 DB 트랜잭션 안에서 이뤄져야 한다.
응답만 사라진 요청의 원문 도해

(원문 출처)
Malcolm Featonby, Making retries safe with idempotent APIs, PDF 4쪽·본문 3쪽의 원문 도해다. 첫 응답이 호출자에게 도달하지 않아도 재요청은 같은 식별자를 유지한다. 서비스는 이미 처리한 요청임을 확인하고 의미가 같은 응답을 준비한다. 도해만 발췌했으며 표기와 화살표는 유지했다. 원문 PDF, © Amazon Web Services.
효과를 중복 적용하지 않는 성질과 응답을 재사용하는 계약은 나눠볼 수 있다. 두 번째 호출에 "이미 존재함"을 반환해도 효과는 한 번일 수 있지만, 호출자가 첫 성공 결과를 회복하려면 어떤 결과를 다시 받을지까지 정해야 한다. 위 예제는 저장된 주문 번호를 다시 주는 계약을 택했다.
키를 지운 뒤에도 같은 보장을 할 수 있을까
운영에서는 키를 어느 사용자와 작업 종류에 묶을지, 요청 내용이 같다고 판단할 기준은 무엇인지, 기록을 언제까지 남기고 처리 중인 요청은 어떻게 다룰지 정해야 한다. 기록을 지운 뒤 도착한 요청은 과거 요청과 구별할 근거를 잃으므로, 유효한 재전송 기간과 보존 정책을 함께 설계한다. 키가 같고 본문이 다른 경우도 중복 성공으로 덮어서는 안 된다. AWS 원문의 늦게 도착한 요청과 다른 의도
DB 밖의 결제·메일·파일 쓰기까지 한 DB 트랜잭션이 묶어주지는 않는다. 외부 서비스가 제공하는 멱등 키를 이용하거나, 전송할 사건을 DB에 함께 기록하는 트랜잭셔널 아웃박스와 수신자의 중복 처리를 이어서 설계한다. HTTP 메서드의 멱등성도 메서드가 의도한 효과를 기준으로 설명하며, 응답 본문이나 부수 로그는 호출마다 달라질 수 있다. RFC 9110 §9.2.2