同じキーでも同じリクエストとは限らない
한국어 원문으로 표시합니다.
목표
키 범위·본문 정규화·재사용 충돌을 분리해 재전송을 판정합니다.
왜 중요한가
두 고객이 우연히 같은 멱등 키를 사용하자 한 고객에게 다른 고객의 응답이 반환됐다. 다른 요청에서는 같은 키로 금액을 바꿨는데도 이전 성공을 돌려줬다. 멱등 키 자체만 비교하면 요청의 의미와 보안 경계를 놓친다. 키는 테넌트와 작업 범위에 묶고 본문의 의미는 별도 지문으로 검사해야 한다.
단계
/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
-
/root/work/idem-fingerprint-lab/service.py에서 canonical(body)는 dict만 받아 sort_keys=True, separators=(',',':'), ensure_ascii=False, allow_nan=False인 JSON 문자열로 반환합니다. 직렬화 불가능한 값은 ValueError로 통일합니다. -
/root/work/idem-fingerprint-lab/service.py에서 fingerprint(body)는 canonical(body)의 UTF-8 바이트에 SHA-256을 적용한 64자 hex 문자열입니다. -
/root/work/idem-fingerprint-lab/service.py에서 scoped_key(tenant, method, path, key)는 tenant와 key를 valid_key로 검증하고, method를 대문자로 바꿉니다. path는 /로 시작하는 문자열이어야 합니다. 네 값을 JSON 배열로 separators=(',',':') 인코딩해 반환합니다. -
/root/work/idem-fingerprint-lab/service.py에서 classify(record, digest)는 record=None이면 'new', record['fingerprint']==digest면 'replay', 아니면 'conflict'입니다. -
/root/work/idem-fingerprint-lab/service.py에서 remember(records, key, digest, response)는 새 키에 {fingerprint:digest, response:response의 deepcopy}를 저장합니다. 이미 있으면 ValueError이고 기존 기록은 보존합니다. -
/root/work/idem-fingerprint-lab/service.py에서 replay(record)는 record['response']의 깊은 사본입니다. -
/root/work/idem-fingerprint-lab/service.py에서 execute(records, tenant, method, path, key, body, action)는 scope와 지문을 계산합니다. new면 action() 결과를 remember하고 사본 반환, replay면 기존 응답 사본 반환, conflict면 ValueError입니다. action 예외는 전파하고 기록을 남기지 않습니다.
참고
- 인터넷과 패키지 설치 없이 기존 lab-dev 환경에서 수행합니다.
- 각 단계는 45초 채점 예산 안에서 실행됩니다. 실제 sleep이나 네트워크 호출을 추가하지 마세요.
- 채점은 제출 모듈을 새로 불러오고 독립 입력과 임시 DB로 검사합니다. 예상값을 상수로 반환하는 대신 계약을 구현하세요.
- FastAPI 공식 문서 · pytest 공식 문서 · Python sqlite3
- 한계: 이 실습은 단일 프로세스 메모리 사전으로 키와 요청의 의미를 분리하는 계약을 배운다. 프로세스 장애 후 보존이나 여러 워커의 동시성은 다음 SQLite 실습에서 다룬다. 해시는 암호화가 아니며 JSON 정규화가 모든 언어의 숫자 표현까지 표준화하는 국제 규격이라고 주장하지 않는다.
키 문법을 검증한다
/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로 확인하세요.