LabHub
배우기 러닝패스 코스

冪等性 — 二度押しても決済は一度だけ

同じキーでも同じリクエストとは限らない

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

키 범위·본문 정규화·재사용 충돌을 분리해 재전송을 판정합니다.

왜 중요한가

두 고객이 우연히 같은 멱등 키를 사용하자 한 고객에게 다른 고객의 응답이 반환됐다. 다른 요청에서는 같은 키로 금액을 바꿨는데도 이전 성공을 돌려줬다. 멱등 키 자체만 비교하면 요청의 의미와 보안 경계를 놓친다. 키는 테넌트와 작업 범위에 묶고 본문의 의미는 별도 지문으로 검사해야 한다.

단계

  1. /root/work/idem-fingerprint-lab/service.py에서 valid_key(value)는 영문·숫자·밑줄·하이픈 1~64자만 그대로 반환하고 다른 입력은 ValueError입니다.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab
  1. /root/work/idem-fingerprint-lab/service.py에서 canonical(body)는 dict만 받아 sort_keys=True, separators=(',',':'), ensure_ascii=False, allow_nan=False인 JSON 문자열로 반환합니다. 직렬화 불가능한 값은 ValueError로 통일합니다.

  2. /root/work/idem-fingerprint-lab/service.py에서 fingerprint(body)는 canonical(body)의 UTF-8 바이트에 SHA-256을 적용한 64자 hex 문자열입니다.

  3. /root/work/idem-fingerprint-lab/service.py에서 scoped_key(tenant, method, path, key)는 tenant와 key를 valid_key로 검증하고, method를 대문자로 바꿉니다. path는 /로 시작하는 문자열이어야 합니다. 네 값을 JSON 배열로 separators=(',',':') 인코딩해 반환합니다.

  4. /root/work/idem-fingerprint-lab/service.py에서 classify(record, digest)는 record=None이면 'new', record['fingerprint']==digest면 'replay', 아니면 'conflict'입니다.

  5. /root/work/idem-fingerprint-lab/service.py에서 remember(records, key, digest, response)는 새 키에 {fingerprint:digest, response:response의 deepcopy}를 저장합니다. 이미 있으면 ValueError이고 기존 기록은 보존합니다.

  6. /root/work/idem-fingerprint-lab/service.py에서 replay(record)는 record['response']의 깊은 사본입니다.

  7. /root/work/idem-fingerprint-lab/service.py에서 execute(records, tenant, method, path, key, body, action)는 scope와 지문을 계산합니다. new면 action() 결과를 remember하고 사본 반환, replay면 기존 응답 사본 반환, conflict면 ValueError입니다. action 예외는 전파하고 기록을 남기지 않습니다.

참고

키 문법을 검증한다

/root/work/idem-fingerprint-lab/service.py에서 valid_key(value)는 영문·숫자·밑줄·하이픈 1~64자만 그대로 반환하고 다른 입력은 ValueError입니다.

처음 한 번 준비하세요. 기존 파일은 덮어쓰지 않습니다.

mkdir -p /root/work/idem-fingerprint-lab
test -e /root/work/idem-fingerprint-lab/service.py || cp /opt/fixtures/ten_labs/idem-fingerprint-lab/service.py /root/work/idem-fingerprint-lab/service.py
cd /root/work/idem-fingerprint-lab

키 길이와 허용 문자를 제한하고 빈 키를 정상 재전송으로 처리하지 않습니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/01-contract.sh로 확인하세요.

객체 순서는 접고 배열 순서는 보존한다

/root/work/idem-fingerprint-lab/service.py에서 canonical(body)는 dict만 받아 sort_keys=True, separators=(',',':'), ensure_ascii=False, allow_nan=False인 JSON 문자열로 반환합니다. 직렬화 불가능한 값은 ValueError로 통일합니다.

배열을 정렬하면 사용자가 요청한 작업 순서를 바꾸게 됩니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/02-contract.sh로 확인하세요.

본문 지문을 계산한다

/root/work/idem-fingerprint-lab/service.py에서 fingerprint(body)는 canonical(body)의 UTF-8 바이트에 SHA-256을 적용한 64자 hex 문자열입니다.

파이썬 hash()는 프로세스마다 바뀌므로 저장 지문으로 쓰지 않습니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/03-contract.sh로 확인하세요.

테넌트와 작업으로 키를 구분한다

/root/work/idem-fingerprint-lab/service.py에서 scoped_key(tenant, method, path, key)는 tenant와 key를 valid_key로 검증하고, method를 대문자로 바꿉니다. path는 /로 시작하는 문자열이어야 합니다. 네 값을 JSON 배열로 separators=(',',':') 인코딩해 반환합니다.

단순 구분자 이어붙이기보다 구조를 인코딩해야 경계가 명확합니다. 경로의 대소문자는 보존합니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/04-contract.sh로 확인하세요.

세 가지 판정을 구분한다

/root/work/idem-fingerprint-lab/service.py에서 classify(record, digest)는 record=None이면 'new', record['fingerprint']==digest면 'replay', 아니면 'conflict'입니다.

키가 존재한다는 이유만으로 모든 재요청을 성공으로 재생하지 않습니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/05-contract.sh로 확인하세요.

응답을 저장할 때 복사한다

/root/work/idem-fingerprint-lab/service.py에서 remember(records, key, digest, response)는 새 키에 {fingerprint:digest, response:response의 deepcopy}를 저장합니다. 이미 있으면 ValueError이고 기존 기록은 보존합니다.

응답 안의 리스트도 복사하지 않으면 중첩 상태가 공유됩니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/06-contract.sh로 확인하세요.

응답을 읽을 때도 복사한다

/root/work/idem-fingerprint-lab/service.py에서 replay(record)는 record['response']의 깊은 사본입니다.

첫 응답을 수정한 호출자가 다음 재전송 결과까지 바꾸지 못하게 합니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/07-contract.sh로 확인하세요.

업무 함수를 한 번만 호출한다

/root/work/idem-fingerprint-lab/service.py에서 execute(records, tenant, method, path, key, body, action)는 scope와 지문을 계산합니다. new면 action() 결과를 remember하고 사본 반환, replay면 기존 응답 사본 반환, conflict면 ValueError입니다. action 예외는 전파하고 기록을 남기지 않습니다.

업무 함수 호출 횟수와 실패 뒤 남은 기록까지 검사해야 재전송 계약을 알 수 있습니다.

저장 후 bash /opt/lab/checks/idem-fingerprint-lab/08-contract.sh로 확인하세요.