Zettelkasten

복식부기는 모든 거래에서 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 패턴을 쓰는 분산 결제 시스템까지)에 여전히 표준인 이유는, "돈은 사라지지 않고 어딘가로 이동한다"는 물리 법칙을 데이터 모델로 강제하기 때문이다. 자가검증 가능한 불변식은 분산 환경에서 더 가치 있어진다.

관련 노트

참고

원장(Ledger) 설계:

분산 트랜잭션 / 보상 패턴:

멱등성: