21. 게임 재화 원장(ledger) 설계: 이중부기·잔액 스냅샷·감사와 멱등성
난이도 중내 답안
모범답안
문제 21 해설 — 게임 재화 원장(ledger) 설계
난이도: 중상
1. 잔액 컬럼 단일 UPDATE의 한계
UPDATE users SET gold = gold - 100 WHERE id=? 방식은 간단하지만:
- 추적 불가: "언제, 왜, 얼마가" 변했는지 기록이 없다. 고객 분쟁("골드가 사라졌어요")에 답할 수 없다.
- 감사 불가: 어뷰징(복제·비정상 획득) 탐지, 경제 지표(획득/소모 흐름) 산출이 어렵다.
- 동시성 갱신 유실: load-modify-write 로 하면 lost update. 원자 UPDATE(
gold = gold - @p WHERE gold >= @p)로 일부 해결되지만 이력은 여전히 없다. - 롤백/보상 곤란: 버그로 잘못 지급했을 때 무엇을 되돌려야 할지 근거가 없다.
- 음수/불변식 위반: 잔액이 곧 진실이라 잘못되면 복구 불가.
2. 이중부기(double-entry) 원장 모델
모든 재화 이동을 불변(append-only) 거래 기록으로 남긴다. 회계의 이중부기처럼 각 거래는 어느 계정에서 나가(차변) 어느 계정으로 들어가는지(대변) 를 명시하고, 시스템 전체의 합은 항상 보존된다. 재화의 생성(운영 지급)·소멸(구매로 소각)은 "시스템/민트" 같은 특수 계정을 상대로 기록해 총합 보존을 유지한다.
최소 스키마(거래 라인 기준):
ledger_entries
id BIGINT PK -- 원장 라인 고유 ID(단조 증가)
txn_id UUID/STRING -- 하나의 논리 거래(멱등 키). 한 거래가 여러 라인
account_id BIGINT -- 계정(유저 지갑, 시스템 민트, 상점 소각 계정 등)
currency SMALLINT -- 골드/캐시/이벤트 재화 구분
delta BIGINT -- +입금 / -출금 (정수 최소단위, float 금지)
balance_after BIGINT NULL -- (선택) 이 계정의 반영 후 잔액(스냅샷 가속용)
reason SMALLINT -- 거래 종류(구매/보상/이체/환불/소각…)
ref STRING NULL -- 주문번호·아이템ID 등 외부 참조
created_at TIMESTAMP
UNIQUE(txn_id, account_id) -- 멱등: 같은 거래·계정 중복 삽입 차단
한 거래(txn)는 최소 두 라인(출금 -X, 입금 +X)으로 구성되고 delta 합이 0(순환 보존)이거나, 생성/소멸이면 특수 계정 라인으로 균형을 맞춘다. 재화는 항상 정수 최소 단위로 저장한다(float 반올림 오차 금지).
3. 잔액 조회 성능 — 스냅샷/롤업
전체 원장을 매번 SUM(delta) 하면 계정당 거래가 쌓일수록 느려진다. 두 가지를 조합한다:
- 잔액 스냅샷(체크포인트): 계정별로
account_balances(account_id, currency, balance, as_of_entry_id)를 두고, 조회는 "스냅샷 + 그 이후 라인의 델타 합"으로 계산. 스냅샷은 배치로 주기 갱신하거나 매 거래 시 증분 갱신. - 증분 갱신 잔액 캐시: 거래를 원장에 기록하는 같은 트랜잭션 안에서
account_balances.balance += delta를 원자적으로 갱신. 원장(진실)과 캐시(빠른 조회)를 한 커밋으로 묶어 정합성 유지.balance_after를 라인에 함께 기록하면 특정 시점 잔액 재구성·검증이 쉬워진다.
정합성 핵심: 잔액 캐시는 원장으로부터 언제든 재계산 가능해야 한다(원장이 SoT, source of truth). 불일치가 의심되면 원장 합산으로 복구.
4. 멱등성과 중복 방지
결제/보상/이체는 네트워크 재시도·중복 클릭으로 여러 번 도착할 수 있다.
- 클라이언트/발신 시스템이 거래마다 고유
txn_id(멱등 키)를 부여. - 원장에
UNIQUE(txn_id, account_id)제약 → 같은 거래 재삽입은 제약 위반으로 무시(ON CONFLICT DO NOTHING). 두 번째 요청은 "이미 처리됨"으로 성공 응답만 반환. - 이로써 재시도가 몇 번이든 정확히 한 번 반영된다. 잔액 증분 갱신도 같은 트랜잭션·같은 멱등 경계 안에서 이뤄져야 한다.
5. 감사·정합성 검증·규제
원장으로 가능해지는 것:
- 거래 추적/분쟁 대응: 계정별 타임라인으로 "언제 왜 얼마" 를 즉시 답변.
- 정기 합계 검증(reconciliation): 전 계정 delta 합이 0(또는 민트/소각 계정과 균형)인지 배치 검증 → 버그·유실을 조기 발견.
- 어뷰징 탐지: 비정상 획득 패턴·복제 흐름을 원장 분석으로 포착.
- 롤백/보상: 잘못된 거래를 역거래(reversing entry) 로 상쇄(원본을 지우지 않고 상쇄 라인을 추가 — 불변성 유지).
- 규제·환불: 결제 재화는 환불·세무·감사 요구가 있어 불변 원장이 사실상 필수.
트레이드오프:
- 저장량·쓰기 증폭: 거래마다 여러 라인 + 잔액 갱신. 인덱스도 늘어난다. → 오래된 원장은 콜드 스토리지로 아카이브, 스냅샷으로 조회 가속.
- 쓰기 경로 복잡도: 원장 삽입 + 잔액 갱신을 한 트랜잭션으로 원자화해야 하며, 크로스 계정/크로스 샤드 이체는 사가/2PC 가 필요.
- 단순 재화(중요도 낮은 임시 포인트)는 원장까지 안 가고 잔액 컬럼으로 둘 수도 있다 — 가치·규제·분쟁 위험이 큰 재화일수록 원장으로.
핵심 요약
- 재화를 잔액 컬럼 하나로 두면 추적·감사·복구가 불가능. 불변 이중부기 원장이 진실의 원천.
- 스키마:
txn_id(멱등) +account_id+delta(정수) +UNIQUE(txn_id, account_id). 잔액은 원장에서 파생되는 캐시/스냅샷. - 조회는 스냅샷+델타 또는 같은 트랜잭션 증분 갱신으로 가속하되 원장이 SoT.
- 멱등 키 + 유니크 제약으로 이중 지급 차단, 롤백은 역거래로. 비용은 저장·쓰기 증폭이며 가치 높은 재화에 우선 적용.