복식부기는 모든 거래에서 debit 합 = credit 합 불변식을 강제해 자금 흐름 정합성을 보장한다
·수정 1회
요약
- 복식부기는 한 거래를 두 줄 이상으로 기록한다. 돈이 빠져나간 계정(credit)과 들어간 계정(debit)을 양쪽 다 적는다.
- 모든 거래에서
SUM(debit) = SUM(credit)이 강제되어 돈이 새거나 복사되는 버그가 구조적으로 차단된다. - 매출/비용/부채 같은 재무 질문이 계정(account) 단위
GROUP BY한 방으로 풀린다.
본문
테이블 구조
CREATE TABLE ledger_entry (
id BIGSERIAL PRIMARY KEY,
tx_id UUID NOT NULL,
account TEXT NOT NULL,
debit NUMERIC,
credit NUMERIC,
created_at TIMESTAMPTZ DEFAULT NOW()
);
한 거래(tx_id) = 여러 행. 각 행은 "어느 계정에 얼마가 이동했는지"를 표현.
예시: 유저 A의 5,500원 다이아 100개 구매
tx_id = P1
| account | debit | credit |
| 회사 통장 | 5500 | | ← 돈 들어옴
| 매출(다이아 판매) | | 5500 | ← 매출 발생
SUM(debit) = 5500, SUM(credit) = 5500 ✅
핵심 불변식
-- 깨진 거래 = 버그
SELECT tx_id, SUM(debit) - SUM(credit) AS diff
FROM ledger_entry
GROUP BY tx_id
HAVING SUM(debit) != SUM(credit);
이 쿼리가 0건이 아니면 데이터 손상 또는 버그. 자가검증이 SQL 한 줄로 가능.
재무 질문이 자연스럽게 풀린다
-- 이번 달 매출
SELECT SUM(credit) FROM ledger_entry
WHERE account = '매출' AND created_at >= '2026-05-01';
-- 환불 총액
SELECT SUM(debit) FROM ledger_entry WHERE account = '환불';
-- 유저들이 보유 중인 총 다이아 (부채)
SELECT SUM(debit) - SUM(credit) FROM ledger_entry
WHERE account LIKE '유저%지갑';
단식부기는 잔액 변화만 기록해 시스템 전체 자금 흐름을 추적하지 못한다에서는 reason 문자열 파싱이 필요했지만, 복식부기는 계정 컬럼 GROUP BY로 끝.
왜 다들 안 쓰는가
복잡성 비용이 있다:
- 모든 거래를 "어디서 어디로" 양쪽으로 분개해야 한다 (개발자가 익숙하지 않음)
- 계정 체계(Chart of Accounts)를 사전에 설계해야 한다 (회계 지식 필요)
- 단순 잔액 변경 도메인에는 과한 구조
도메인별 선택 기준
| 도메인 | 선택 |
|---|---|
| 게임 재화, 콘텐츠 크레딧 | 단식부기로 충분 |
| P2P 송금, 지갑 앱 | 복식부기 필요 — A의 -100과 B의 +100이 같은 tx에 묶여야 어뷰징 방지 |
| 은행, 거래소, 핀테크 | 복식부기 + 전용 DB (TigerBeetle, Spanner) — 법적 의무 |
회계 감사 통과
복식부기는 "자산 = 부채 + 자본" 회계 등식을 거래마다 강제한다. 외부 회계 감사를 받게 되면 사실상 필수. IPO 준비나 큰 투자 받을 때 단식부기 시스템은 다시 만들어야 하는 경우가 많다.
700년 된 아이디어
베네치아 상인들이 14세기에 정립한 시스템이 21세기 핀테크 DB(Saga 패턴을 쓰는 분산 결제 시스템까지)에 여전히 표준인 이유는, "돈은 사라지지 않고 어딘가로 이동한다"는 물리 법칙을 데이터 모델로 강제하기 때문이다. 자가검증 가능한 불변식은 분산 환경에서 더 가치 있어진다.
관련 노트
- 단식부기는 잔액 변화만 기록해 시스템 전체 자금 흐름을 추적하지 못한다
- Transactional Outbox 패턴은 DB와 외부 시스템 간 일관성을 보장한다
- Saga 패턴
- 멱등성은 같은 요청을 여러 번 해도 결과가 같음을 보장한다
- 2PC(Two phase Commit)은 분산 트랜잭션에서 원자성을 보장하는 프로토콜이다.
참고
원장(Ledger) 설계:
- https://developer.squareup.com/blog/books-an-immutable-double-entry-accounting-database-service/ — Square: Books, immutable double-entry accounting DB
- https://www.moderntreasury.com/journal/why-we-built-ledgers — Modern Treasury: Why We Built Ledgers
- https://www.moderntreasury.com/journal/how-to-scale-a-ledger-part-v — Modern Treasury: How to Scale a Ledger Part V (Immutability and Double-Entry)
- https://www.moderntreasury.com/journal/designing-ledgers-with-optimistic-locking — Modern Treasury: Designing the Ledgers API with Optimistic Locking
- https://www.uber.com/blog/how-ledgerstore-supports-trillions-of-indexes/ — Uber: How LedgerStore Supports Trillions of Indexes
- https://www.uber.com/us/en/blog/migrating-from-dynamodb-to-ledgerstore/ — Uber: Migrating Ledger Data from DynamoDB to LedgerStore
- https://docs.tigerbeetle.com/concepts/debit-credit/ — TigerBeetle: Debit/Credit Schema for OLTP
- https://docs.tigerbeetle.com/coding/financial-accounting/ — TigerBeetle: Financial Accounting
- https://docs.tigerbeetle.com/ — TigerBeetle Docs 전체
분산 트랜잭션 / 보상 패턴:
- https://ics.uci.edu/~cs223/papers/cidr07p15.pdf — Pat Helland: Life beyond Distributed Transactions (CIDR 2007)
- https://microservices.io/patterns/data/saga.html — microservices.io: Saga 패턴
- https://microservices.io/patterns/data/transactional-outbox.html — microservices.io: Transactional Outbox
멱등성:
- https://stripe.com/blog/idempotency — Stripe: Designing robust and predictable APIs with idempotency
- https://brandur.org/idempotency-keys — Brandur Leach: Implementing Stripe-like Idempotency Keys in Postgres
- https://medium.com/airbnb-engineering/avoiding-double-payments-in-a-distributed-payments-system-2981f6b070bb — Airbnb: Avoiding Double Payments (Orpheus 프레임워크)
- https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/ — AWS Builders' Library: Making retries safe with idempotent APIs