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