먼저 두 명만
- 가와 나가 동시에 잔액을 읽어요. 둘 다 5,000P를 봐요.
- 가: 5,000 + 100 = 5,100P를 적어요.
- 나: 5,000 + 100 = 5,100P를 적어요. 가가 적은 5,100P 위에 덮어써요.
- 있어야 할 잔액
- 5,000 + 100 + 100 = 5,200P
- 실제로 남은 잔액
- 5,100P
- 사라진 포인트
- 100P (가의 몫)
4장
적립과 사용이 동시에 몰리면 포인트 잔액은 맞을까요? 잔액 숫자만 고치는 것과 입출금 내역을 남기는 것, 무엇이 다를까요?
1포인트 통장에는 잔액 숫자 하나만 적혀 있어요.
2두 사람이 동시에 잔액을 읽고, 각자 100P를 더해 적으려 해요.
3둘 다 "5,100"으로 덮어써서, 한 사람 몫 100P가 사라졌어요.
4입출금 내역을 한 줄씩 남기면, 합계로 잔액을 언제든 다시 맞춰 볼 수 있어요.
잔액 숫자만 고치기
통장처럼 내역 남기기
기록 불러오는 중…
…
잔액 숫자만 고쳤더니 사라진 포인트 (200건 동시에)
실제 서버와 데이터베이스에 보내요: 시작 5,000P에서 150명이 100P씩 적립, 50명이 100P씩 사용, 모두 동시에. 하나도 빠짐없이 셌다면 5,000 + 15,000 − 5,000 = 15,000P예요.
있어야 할 잔액은 요청을 하나도 빠짐없이 셌을 때의 값이고, 실제로 남은 잔액은 끝나고 잔액 칸에 적혀 있던 값이에요. 잔액 숫자만 고치는 방식에선 여러 사람이 같은 순간에 같은 옛 잔액을 읽고, 각자 계산한 값을 그 위에 덮어써요. 그래서 먼저 적은 사람의 몫이 지워져요.
기록 불러오는 중…
잔액 숫자만 있으면 틀려도 틀린 줄 몰라요. 입출금 내역을 한 줄씩 남겨 두면, 내역을 전부 더한 값과 잔액 칸을 비교해서(회계 용어로 '대사') 어긋남을 바로 잡아낼 수 있어요.
내역은 다 남았는데 잔액 칸만 틀렸어요. 대사가 없었다면 아무도 몰랐을 거예요.
한 명씩 차례로 처리해서 조금 느리지만, 잔액과 내역이 언제나 같아요.
버튼을 누르면 마지막 실행의 잔액 칸과 내역 합계를 데이터베이스에서 직접 다시 세요.
통장 방식은 잔액을 확인하는 순간부터 다 쓸 때까지 그 통장을 잠가요. 그래서 "잔액 확인 → 사용" 사이에 다른 사용이 끼어들어 잔액이 마이너스가 되는 일이 없어요.
기록 불러오는 중…
PointWallet.balancePointEntry: 추가만 하는 원장 (append-only)SELECT balance … FOR UPDATE(행 잠금) → 0 미만 검사 → INSERT PointEntry → UPDATE balance = balance + ?balance vs SUM(PointEntry.amount)simulatePoints(200)이 POST /points/simulate { requests: 200 }를 보내요.PointsService.simulateBothModes가 덮어쓰기 → 통장 순서로 돌려요. 지갑마다 먼저 잔액 5,000과 opening 내역 한 줄로 리셋해요.applyByOverwrite: 잔액 읽기 → 내역 INSERT → balance = 읽은 값 + amount로 UPDATE (트랜잭션 없음)applyByLedger: 트랜잭션 안에서 FOR UPDATE로 지갑 행 잠금 → 0 미만이면 거절 → 내역 INSERT → balance = balance + amountreconcile()이 잔액 칸과 내역 합계를 비교해요. 통장 쪽은 잔액보다 100 많은 금액을 써 보고 거절되는지도 확인해요.{ results: [overwrite, ledger] }. 요청마다 읽은 시각, 쓴 시각, 본 잔액이 들어 있어요.GET /points/:mode/entries?limit=8로 실제 행을 읽어요.| id | name | balance | updatedAt | SUM(amount) | PointEntry 행 수 |
|---|---|---|---|---|---|
| 47 | sim-overwrite | 4900 | 2026-10-20 05:00:35.157 | 15000 | 201 |
| 48 | sim-ledger | 15000 | 2026-10-20 05:00:35.405 | 15000 | 201 |
200건 실험 직후 읽었어요. 오른쪽 두 칸은 PointEntry를 센 값이에요.
| id | walletName | amount | reason | createdAt |
|---|---|---|---|---|
| 8875 | sim-overwrite | 5000 | opening | 2026-10-20 05:00:35.080 |
| 8876 | sim-overwrite | 100 | earn | 2026-10-20 05:00:35.121 |
| 8877 | sim-overwrite | 100 | earn | 2026-10-20 05:00:35.121 |
| 8883 | sim-overwrite | -100 | spend | 2026-10-20 05:00:35.121 |
sim-overwrite 지갑의 앞부분이에요. 시각은 UTC예요.
POST /points/simulate 한 건이에요. 요청 200개는 서버 안에서 동시에 돌아요.reconciliation.difference가 0이 아니면 장부와 잔액이 어긋난 거예요.-- 통장 방식 (한 트랜잭션) SELECT balance FROM PointWallet WHERE name = ? FOR UPDATE; -- 다른 요청은 여기서 대기 -- balance + amount < 0 이면 거절 INSERT INTO PointEntry (walletName, amount, reason) VALUES (?, ?, ?); UPDATE PointWallet SET balance = balance + ? WHERE name = ?; COMMIT; -- 대사 SELECT w.balance, (SELECT SUM(amount) FROM PointEntry e WHERE e.walletName = w.name) AS entriesSum FROM PointWallet w WHERE w.name = ?;
PointEntry 최근 행 (실제 DB)
sim-overwrite
| id | amount | reason | createdAt |
|---|---|---|---|
| 아직 안 불러왔어요 | |||
sim-ledger
| id | amount | reason | createdAt |
|---|---|---|---|
| 아직 안 불러왔어요 | |||