LabHub
배우기 러닝패스 코스

銀行現場の言葉

二つの窓口が同じ残高を見た — 同時出金と限度額

LabHub 에서 이어서 보기

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

목표

같은 계좌에 겹쳐 들어온 출금으로 마이너스 잔액을 재현 가능하게 만들어 보고, 확인과 갱신을 한 문장에 합쳐 그 틈을 없앤다. 일일 누적 한도를 같은 방식으로 걸고, 잠금 경합에만 상한 있는 재시도를 붙인 뒤, 불변식 점검과 증거 보고서로 닫는다.

왜 중요한가

잔액을 읽어 확인하고 나서 따로 차감하는 구조는 한 요청씩 들어오는 동안에는 영원히 정상이다. 같은 계좌에 요청이 겹치는 순간 둘 다 같은 잔액을 근거로 승인되고, 차감은 두 번 다 반영돼 잔액이 음수가 된다. 이때 원장과 잔액은 서로 어긋나지 않아서 "원장 합계 == 잔액" 만 보는 마감 점검은 이 사고를 통과시킨다. 부하를 걸어 재현하려 들면 확률에 기대게 된다. 이 실습은 부하 대신 겹치는 지점을 맞추는 방법으로 결정적인 재현을 만든다. 요청 마흔 건이 1초도 걸리지 않고, 돌릴 때마다 같은 숫자가 나온다. 채점기는 여러분의 문장을 믿지 않는다. 남긴 기록 파일의 값을 실물 DB 와 하나씩 대조한다. 채점기가 경쟁을 다시 일으키지는 않는다 — 재현이 확률이면 채점도 흔들리기 때문이다.

단계

  1. /root/limit/make_bank.py 를 만들어 실행해 /root/limit/bank.db 에 계좌 6개와 개시 원장 6줄을 넣는다.
  2. /root/limit/race_naive.py 로 읽고-고치고-쓰기 경로를 만들어 A-RACE 계좌에 2명 x 20라운드로 겹쳐 돌리고 결과를 /root/limit/race_naive.json 에 남긴다.
  3. /root/limit/transfer.py 에 조건부 갱신 경로를 만들어 A-SAFE 계좌에 같은 조건으로 돌리고 결과를 /root/limit/race_safe.json 에 남긴다.
  4. A-FEE 계좌로 쓰기 잠금을 직접 겪어 보고 busy_timeout 0과 넉넉한 값의 차이를 /root/limit/lock_probe.json 에 남긴다.
  5. A-DAILY 계좌에 일일 누적 한도 300000원을 갱신 문장 안에서 걸고 결과를 /root/limit/daily_limit.json 에 남긴다.
  6. A-RETRY 계좌에 3명 x 10라운드로 요청을 넣고, 잠금 경합에만 거는 상한 있는 재시도 정책과 그 결과를 /root/limit/retry_policy.json 에 남긴다.
  7. 계좌 6개에 불변식 세 가지를 걸어 위반 목록을 /root/limit/invariant.json 에 남긴다.
  8. 여기까지의 숫자를 원장에서 뽑아 /root/limit/limit_report.md 에 네 절로 정리한다.

참고

계좌 스냅샷 만들기

/root/limit/make_bank.py 를 만들어 실행해 /root/limit/bank.db 에 account, ledger, attempt 세 표와 계좌 6개, 개시 원장 6줄을 넣으세요.

계좌는 A-RACE 150000/100000, A-SAFE 150000/200000, A-DAILY 1000000/300000, A-RETRY 300000/500000, A-FEE 80000/50000, A-POOL 1950000/5000000 입니다(잔액/일일한도). 개시 줄은 path 를 'OPEN' 으로 두고 biz_date 는 2026-09-17 로 맞춥니다. 뒤 단계가 이 줄을 건드리지 않아야 다시 채점해도 같은 답이 나옵니다.

겹치는 두 요청으로 마이너스 잔액 재현하기

/root/limit/race_naive.py 를 만들어 A-RACE 계좌에 작업자 2명 x 20라운드 x 10000원으로 겹쳐 돌리고, 결과를 /root/limit/race_naive.json 에 남기세요.

잔액을 읽고, 읽은 값으로 판단하고, 그 뒤에 balance = balance - amount 로 갱신하는 순서를 그대로 두세요. 라운드마다 두 작업자가 모두 읽을 때까지 기다리게 하면 같은 잔액을 보게 됩니다. 기록에는 승인/거절 수, 승인 합계, 마지막 잔액을 담고 attempt 표에도 요청마다 한 줄씩 남기세요.

확인과 갱신을 한 문장에 넣기

/root/limit/transfer.py 를 만들어 A-SAFE 계좌에 2단계와 같은 조건으로 돌리고, 결과를 /root/limit/race_safe.json 에 남기세요.

잔액 조건을 UPDATE 의 WHERE 에 붙이고, 승인 여부는 바뀐 행 수로 판정합니다. 읽어 둔 잔액은 기록(seen_balance)에만 쓰세요. 읽을 때는 충분해 보였는데 거절된 요청이 몇 건인지가 이 단계의 증거입니다.

쓰기 잠금과 busy_timeout 직접 겪어 보기

A-FEE 계좌에서 한쪽이 BEGIN IMMEDIATE 로 잠금을 쥐고 있는 동안 같은 갱신을 busy_timeout 0과 넉넉한 값으로 각각 시도해 /root/limit/lock_probe.json 에 남기세요.

쥐는 쪽은 1000원, 기다리는 쪽은 2000원을 빼고 path 는 'lock' 으로 적습니다. busy_timeout 이 0이면 즉시 실패하고, 넉넉하면 잠금이 풀릴 때까지 잠들었다 성공합니다. 막힌 시도도 attempt 표에 decision='busy' 로 남기세요 — 기록이 없으면 무슨 일이 있었는지 알 수 없습니다.

일일 누적 한도를 같은 문장 안에 넣기

A-DAILY 계좌에 2명 x 10라운드 x 50000원을 넣고, 일일 한도 300000원을 갱신 문장 안에서 판정해 결과를 /root/limit/daily_limit.json 에 남기세요.

누적액을 따로 칸에 들고 있으면 그 칸과 원장이 어긋나는 순간 한도가 거짓말을 합니다. 그날 출금 합계를 원장에서 세는 하위 질의를 UPDATE 의 조건에 넣으세요. 거절 사유는 잔액 부족과 한도 초과를 구분해 남깁니다.

재시도를 정책으로 적고 상한 지키기

A-RETRY 계좌에 3명 x 10라운드 x 20000원을 넣고, 잠금 경합에만 거는 상한 있는 재시도 정책과 그 결과를 /root/limit/retry_policy.json 에 남기세요.

busy_timeout 을 0으로 두면 경합이 예외로 올라오니 그것만 붙잡아 다시 겁니다. 잔액 부족은 몇 번을 다시 걸어도 같은 답이라 걸면 안 됩니다. 요청마다 몇 번 시도했는지를 attempt.attempts 에 남기고, 정책의 상한을 넘은 시도가 없어야 합니다.

불변식 세 가지로 계좌 여섯 개 점검하기

계좌 6개에 balance_matches_ledger, no_negative_balance, daily_limit_respected 세 가지를 걸어 위반 목록을 /root/limit/invariant.json 에 남기세요.

첫 번째 불변식만 보면 2단계의 사고가 통과합니다 — 승인된 만큼 원장에도 다 적혔기 때문입니다. 위반마다 계좌, 어떤 불변식인지, 관측값(observed), 기준값(limit)을 함께 적으세요. 채점기는 같은 계산을 DB 에서 다시 해 목록을 대조합니다.

증거 보고서로 닫기

/root/limit/limit_report.md 에 '## 무엇이 났는가', '## 어떻게 재현했는가', '## 어떻게 막았는가', '## 남은 위험과 운영 규칙' 네 절을 두고 원장에서 뽑은 숫자와 함께 적으세요.

숫자를 손으로 옮겨 적으면 한 번은 맞고 다음 실행부터 틀립니다. DB 를 읽어 보고서를 만드는 스크립트를 쓰세요. 무엇이 났는가 절에는 마지막 잔액과 초과로 나간 금액, 재현 절에는 겹쳐 돌린 요청 수, 막은 절에는 조건부 갱신으로 승인된 건수, 마지막 절에는 당일 한도까지 나간 금액과 재시도를 포함한 시도 합계가 있어야 합니다.