重複受信の防御を実装する
한국어 원문으로 표시합니다.
목표
이중 청구를 직접 만들어 본 뒤 멱등성 키로 막고, 동시 요청과 본문 불일치까지 처리해 실제 결제 API 수준의 방어를 완성한다.
왜 중요한가
네트워크를 가로지르는 전달에서 순수한 의미의 정확히 한 번은 불가능에 가깝습니다. 보낸 쪽은 확인 응답이 안 왔을 때 "도착 못 함"과 "도착했는데 응답만 유실"을 구별할 수 없기 때문입니다. 그래서 실무의 정답은 정해져 있습니다 — 적어도 한 번으로 보내고, 받는 쪽에서 중복을 지웁니다. 이 실습에서 가장 미묘한 부분은 5번 스텝입니다. 키를 조회한 뒤 없으면 저장하는 순진한 구현은 두 요청이 동시에 도착하면 둘 다 통과합니다. Redis 의 SET ... NX 한 번으로 조회와 선점을 원자적으로 합쳐야 합니다. 이 한 줄의 차이가 프로모션 때 이중 청구 신고가 들어오느냐 마느냐를 가릅니다.
단계
/root/idem/pay.py를 127.0.0.1:8130 에 띄운다. 시작 시 Redis 키balance를 10000 으로 초기화하고,POST /pay는{"amount":100}을 받아 잔액을 깎고{"charged":100,"balance":<잔액>}을 준다.- 멱등성 없이 같은 요청을 2번 보낸 결과를
/root/idem/double.out에balance_before=10000 balance_after=9800형식으로 적는다. pay.py가Idempotency-Key헤더를 요구하게 바꾼다. 헤더가 없으면 400 을 준다.- 같은 키로 2번 보내면 첫 응답이 그대로 반복되고 잔액은 한 번만 깎인다.
/root/idem/dedup.out에balance_after=9900 calls=2를 적는다. - 같은 키로 동시에 2건을 보내도 한 번만 처리된다.
/root/idem/concurrent.out에processed=1 balance_delta=100을 적는다. - 같은 키에 다른 금액을 보내면 422 를 준다.
/root/idem/mismatch.out에status=422를 적는다. - 멱등성 키의 남은 수명이 0보다 크고 86400 이하여야 한다.
/root/idem/ttl.txt에ttl=<초>를 적는다.
참고
- 원자적 선점:
SET idem:<key> IN_PROGRESS NX EX 86400— 이 명령이 0을 돌려주면 이미 누군가 선점한 것입니다. - 동시 요청 만들기:
curl ... & curl ... & wait - 확정적 실패(잔액 부족)는 응답을 캐시하고, 일시적 실패(타임아웃)는 키를 풀어 재시도가 진짜로 다시 시도되게 합니다.
- 흔한 실수: 조회 후 저장하는 2단계 구현 — 그 사이가 경쟁 구간입니다.
결제 서비스 띄우기
/root/idem/pay.py 를 127.0.0.1:8130 에 띄운다. 시작 시 Redis 키 balance 를 10000 으로 초기화하고, POST /pay 는 {"amount":100} 을 받아 잔액을 깎고 {"charged":100,"balance":<잔액>} 을 준다.
잔액을 Redis 에 두면 다시 시작해도 상태가 유지됩니다. 처음 잔액을 정해진 값으로 초기화하세요.
이중 청구 재현하기
멱등성 없이 같은 요청을 2번 보낸 결과를 /root/idem/double.out 에 balance_before=10000 balance_after=9800 형식으로 적는다.
같은 결제 요청을 두 번 보내면 잔액이 두 번 깎입니다. 이것이 막아야 할 상태입니다.
멱등성 키 헤더 받기
pay.py 가 Idempotency-Key 헤더를 요구하게 바꾼다. 헤더가 없으면 400 을 준다.
헤더가 없으면 400 으로 거절하는 편이 안전합니다. 클라이언트가 키를 만들도록 강제하는 것이 계약입니다.
같은 키는 한 번만 처리하기
같은 키로 2번 보내면 첫 응답이 그대로 반복되고 잔액은 한 번만 깎인다. /root/idem/dedup.out 에 balance_after=9900 calls=2 를 적는다.
키에 대해 저장해 둔 첫 응답을 그대로 돌려줍니다. 두 번째 요청에서 잔액이 변하지 않아야 합니다.
동시 요청에서도 한 번만 처리하기
같은 키로 동시에 2건을 보내도 한 번만 처리된다. /root/idem/concurrent.out 에 processed=1 balance_delta=100 을 적는다.
조회 후 저장하면 그 사이에 다른 요청이 끼어듭니다. 한 번의 원자적 명령으로 선점해야 합니다.
키는 같은데 본문이 다르면 거부하기
같은 키에 다른 금액을 보내면 422 를 준다. /root/idem/mismatch.out 에 status=422 를 적는다.
첫 요청의 본문 해시를 함께 저장해 두면 비교할 수 있습니다. 상태 코드는 422 입니다.
키 만료 설정하기
멱등성 키의 남은 수명이 0보다 크고 86400 이하여야 한다. /root/idem/ttl.txt 에 ttl=<초> 를 적는다.
키를 영원히 보관하면 메모리가 무한히 자랍니다. 24시간이 관행입니다. 남은 수명을 조회해 확인하세요.