LabHub
배우기 러닝패스 코스

統合とデプロイ

応答を失う決済APIを相手にする

LabHub 에서 이어서 보기

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

목표

타임아웃 뒤의 재시도가 어떻게 중복 결제를 만드는지 직접 만들어 보고, 멱등키로 막습니다. 그리고 키를 붙였는데도 안 되는 경우까지 확인합니다.

환경

/opt/app/pay.py 는 고객사 결제 API 를 흉내 낸 것입니다. 고칠 대상이 아니라 상대편입니다.

mkdir -p /root/idem
nohup python3 /opt/app/pay.py > /tmp/pay.log 2>&1 &
sleep 1
curl -s http://127.0.0.1:8021/health
POST /payments    결제 하나를 기록한다
                  Idempotency-Key 헤더가 있고 이미 본 키면
                  새로 기록하지 않고 저장해 둔 결과를 그대로 돌려준다
GET  /payments    지금까지 기록된 결제 전부
POST /reset       상태 초기화

짝수 번째 요청은 처리를 끝내고 응답만 6초 늦춥니다. 2초로 타임아웃을 걸면 클라이언트는 실패로 보지만 서버에는 이미 기록돼 있습니다. 이것이 "불확실한 결과" 입니다.

만들 것

전부 /root/idem/ 아래입니다.

naive.sh      멱등키 없이. 타임아웃이면 다시 보낸다
naive.txt     그 결과와 왜 그런지
safe.sh       비즈니스 행위 하나에 키 하나. 재시도에도 같은 키
newkey.sh     시도마다 새 키를 만든다 (일부러 틀린 형태)
newkey.txt    그 결과와 키를 무엇 단위로 만들어야 하는지
backoff.sh    대기 시간 간격을 낸다
retryable.sh  상태를 받아 재시도 여부를 답한다
payments.csv  조사할 결제 기록
forensics.txt 중복을 찾고 원인을 지목한 결과
report.md     정리

채점 방식

채점기가 매번 서버를 초기화하고 여러분의 스크립트를 직접 돌린 뒤 기록을 셉니다. 적어 놓은 숫자가 아니라 실제로 남은 기록을 봅니다.

naive.sh   기록 > 주문  이어야 한다 (중복이 생겨야 한다)
safe.sh    기록 = 주문  이어야 한다 (정확히 한 번)
newkey.sh  기록 > 주문  이어야 한다 (키가 무력해진다)
backoff.sh 간격이 늘고, 두 번 돌리면 달라야 한다

단계

  1. 결제 API 를 띄웁니다.
  2. naive.sh — 멱등키 없이 주문 4건 이상을 결제하고, 타임아웃이면 다시 보냅니다. 결과를 naive.txt 에 적습니다.
  3. safe.sh — 주문마다 키 하나를 정하고, 재시도할 때 같은 키를 다시 씁니다.
  4. newkey.sh — 키를 붙이되 시도마다 새로 만듭니다. newkey.txt 에 왜 소용없는지 적습니다.
  5. backoff.sh — 재시도 간격을 4개 이상 냅니다. 지수적으로 늘고, 무작위가 섞여 있어야 합니다.
  6. retryable.sh — 상태(500, 429, 400, timeout 등)를 인자로 받아 yes 또는 no 를 냅니다.
  7. payments.csv 를 만들고 중복을 찾습니다. 중복 건들의 시각 간격이 원인을 말해 줍니다.
  8. 정리합니다.

참고

7단계의 재료는 이렇게 만듭니다.

cat > /root/idem/payments.csv <<'CSV'
payment_id,order_id,created_at
PAY-1,ORD-1,2026-09-07T10:00:00Z
PAY-2,ORD-2,2026-09-07T10:00:05Z
PAY-3,ORD-2,2026-09-07T10:00:06Z
PAY-4,ORD-2,2026-09-07T10:00:08Z
PAY-5,ORD-2,2026-09-07T10:00:12Z
PAY-6,ORD-3,2026-09-07T10:01:00Z
PAY-7,ORD-4,2026-09-07T10:02:00Z
PAY-8,ORD-4,2026-09-07T10:02:01Z
PAY-9,ORD-4,2026-09-07T10:02:03Z
PAY-10,ORD-4,2026-09-07T10:02:07Z
CSV

간격이 1초 · 2초 · 4초로 배가 되는 것이 보이면 거의 확정입니다.

응답을 잃어버리는 API 띄우기

결제 API 를 띄웁니다.

/opt/app/pay.py 를 백그라운드로 띄우고 /health 가 200 인지 봅니다. 시작 전에 /reset 으로 상태를 비웁니다.

타임아웃을 실패로 보면

naive.sh — 멱등키 없이 주문 4건 이상을 결제하고, 타임아웃이면 다시 보냅니다. 결과를 naive.txt 에 적습니다.

curl --max-time 2 로 타임아웃을 걸고, 실패하면 || 로 한 번 더 보냅니다. 주문 4건 이상. 그런 다음 GET /payments 로 실제 기록을 세어 보세요.

멱등키로 정확히 한 번

safe.sh — 주문마다 키 하나를 정하고, 재시도할 때 같은 키를 다시 씁니다.

주문마다 키를 하나 정하고, 재시도에도 같은 키를 씁니다. -H "Idempotency-Key: $K" 를 두 호출 모두에 붙이세요.

키를 붙였는데도 안 되는 경우

newkey.sh — 키를 붙이되 시도마다 새로 만듭니다. newkey.txt 에 왜 소용없는지 적습니다.

시도마다 새 키를 만들어 보세요. 서버는 다른 요청으로 보고 또 기록합니다. 키는 'HTTP 요청 하나' 가 아니라 '비즈니스 행위 하나' 에 대해 만듭니다.

간격을 늘리고 흩뜨린다

backoff.sh — 재시도 간격을 4개 이상 냅니다. 지수적으로 늘고, 무작위가 섞여 있어야 합니다.

간격이 매번 커져야 하고(지수 백오프), 두 번 돌리면 값이 달라야 합니다(지터). 지터가 없으면 모든 클라이언트가 같은 시각에 동시에 재시도합니다.

무엇을 재시도하고 무엇을 안 하나

retryable.sh — 상태(500, 429, 400, timeout 등)를 인자로 받아 yes 또는 no 를 냅니다.

요청이 잘못된 것(400·422)과 자격증명 문제(401·403)는 몇 번을 보내도 같습니다. 429 는 재시도하되 Retry-After 를 지킵니다.

간격이 원인을 말해 준다

payments.csv 를 만들고 중복을 찾습니다. 중복 건들의 시각 간격이 원인을 말해 줍니다.

중복된 주문을 찾고, 그 기록들의 시각 차이를 보세요. 1초 · 2초 · 4초처럼 배로 늘면 클라이언트 재시도입니다.

정리

정리합니다.

타임아웃이 무엇을 뜻하는지, 키를 무엇 단위로 만드는지, 백오프와 지터가 각각 무엇을 막는지 적으세요.