LabHub
배우기 러닝패스 코스

Idempotency — Two Clicks, One Charge

Preserve balances when a transfer fails halfway

LabHub 에서 이어서 보기

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

목표

차감·증가·이체 기록을 원자적으로 묶고 동시 잔액 경쟁을 재현합니다.

왜 중요한가

송신 계좌에서는 돈을 빼고 수신 계좌에 더하기 전에 프로세스가 죽었다. 재시도는 이체 id가 없어서 다시 돈을 뺐다. 반대로 이체 기록만 먼저 커밋하면 재시도는 완료라고 판단해 입금이 영원히 누락된다. 금액 계산을 정수로 정확히 해도 트랜잭션 경계가 틀리면 결과가 잘못된다.

단계

  1. /root/work/idem-ledger-transfer-lab/service.py에서 init_db(path)는 accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL)와 transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)를 멱등 생성합니다.

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

mkdir -p /root/work/idem-ledger-transfer-lab
test -e /root/work/idem-ledger-transfer-lab/service.py || cp /opt/fixtures/ten_labs/idem-ledger-transfer-lab/service.py /root/work/idem-ledger-transfer-lab/service.py
cd /root/work/idem-ledger-transfer-lab
  1. /root/work/idem-ledger-transfer-lab/service.py에서 add_account(path, account_id, amount)는 bool 제외 0 이상 int만 허용하고 INSERT로 계좌를 만듭니다. 이미 있는 계좌는 sqlite3.IntegrityError로 거절하며 잔액은 보존합니다.

  2. /root/work/idem-ledger-transfer-lab/service.py에서 balance(path, account_id)는 계좌 잔액을 반환하고 없으면 KeyError입니다.

  3. /root/work/idem-ledger-transfer-lab/service.py에서 validate_transfer(source,target,amount)는 source!=target이고 amount가 bool 제외 양의 int이면 None, 아니면 ValueError입니다.

  4. /root/work/idem-ledger-transfer-lab/service.py에서 transfer(path,tx_id,source,target,amount,fault=lambda:None)는 같은 id/내용이면 False, id 충돌·없는 계좌·부족한 잔액은 ValueError입니다. 새 이체는 차감→fault()→입금→transfers 삽입을 원자적으로 실행하고 True입니다.

  5. /root/work/idem-ledger-transfer-lab/service.py에서 history(path)는 transfers를 id 오름차순의 (id,source,target,amount) 튜플 리스트로 반환합니다.

  6. /root/work/idem-ledger-transfer-lab/service.py에서 total_balance(path)는 accounts.balance의 합계입니다. 계좌가 없으면 0입니다.

  7. /root/work/idem-ledger-transfer-lab/service.py에서 compete(path,source,target,amount)는 서로 다른 id race-a/race-b로 같은 금액의 transfer를 두 스레드에서 실행합니다. ValueError만 False로 변환한 결과 리스트를 반환합니다. 잔액이 한 번분만 있으면 성공은 하나입니다.

참고

원장 테이블을 만든다

/root/work/idem-ledger-transfer-lab/service.py에서 init_db(path)는 accounts(id TEXT PRIMARY KEY,balance INTEGER NOT NULL)와 transfers(id TEXT PRIMARY KEY,source TEXT NOT NULL,target TEXT NOT NULL,amount INTEGER NOT NULL)를 멱등 생성합니다.

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

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

같은 이체 id가 두 번 커밋되지 않게 유일성을 DB에 둡니다.

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

계좌를 한 번만 만든다

/root/work/idem-ledger-transfer-lab/service.py에서 add_account(path, account_id, amount)는 bool 제외 0 이상 int만 허용하고 INSERT로 계좌를 만듭니다. 이미 있는 계좌는 sqlite3.IntegrityError로 거절하며 잔액은 보존합니다.

초기화의 UPSERT가 실제 잔액을 초기값으로 되돌리지 않게 합니다.

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

없는 계좌와 0원을 구분한다

/root/work/idem-ledger-transfer-lab/service.py에서 balance(path, account_id)는 계좌 잔액을 반환하고 없으면 KeyError입니다.

없음을 0으로 바꾸면 잘못된 계좌로 이체를 진행할 수 있습니다.

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

이체 입력을 검증한다

/root/work/idem-ledger-transfer-lab/service.py에서 validate_transfer(source,target,amount)는 source!=target이고 amount가 bool 제외 양의 int이면 None, 아니면 ValueError입니다.

동일 계좌의 이체와 음수 이체를 사전에 거절합니다.

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

세 쓰기를 한 트랜잭션으로 묶는다

/root/work/idem-ledger-transfer-lab/service.py에서 transfer(path,tx_id,source,target,amount,fault=lambda:None)는 같은 id/내용이면 False, id 충돌·없는 계좌·부족한 잔액은 ValueError입니다. 새 이체는 차감→fault()→입금→transfers 삽입을 원자적으로 실행하고 True입니다.

차감 뒤 강제로 예외를 내어 잔액과 이체 기록이 모두 원래대로인지 검사합니다.

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

이체 이력을 결정적으로 읽는다

/root/work/idem-ledger-transfer-lab/service.py에서 history(path)는 transfers를 id 오름차순의 (id,source,target,amount) 튜플 리스트로 반환합니다.

입력 순서와 조회 순서를 혼동하지 않도록 ORDER BY를 명시합니다.

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

보존량을 확인한다

/root/work/idem-ledger-transfer-lab/service.py에서 total_balance(path)는 accounts.balance의 합계입니다. 계좌가 없으면 0입니다.

개별 요청의 반환값과 전체 금액 보존을 별도로 검증합니다.

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

동시 이체가 잔액을 넘지 않는다

/root/work/idem-ledger-transfer-lab/service.py에서 compete(path,source,target,amount)는 서로 다른 id race-a/race-b로 같은 금액의 transfer를 두 스레드에서 실행합니다. ValueError만 False로 변환한 결과 리스트를 반환합니다. 잔액이 한 번분만 있으면 성공은 하나입니다.

트랜잭션 밖에서 잔액을 미리 검사하면 두 요청이 같은 잔액을 보고 모두 성공할 수 있습니다.

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