Skip to content

필사 모드: 은행 원장 설계 — 복식부기, 이벤트 소싱, 그리고 1원의 정합성

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — 1원이 틀리면 은행이 멈춘다

은행 시스템을 처음 접하는 개발자가 가장 놀라는 지점은 "잔액이 1원이라도 안 맞으면 마감이 안 된다"는 사실입니다. 일반적인 웹 서비스라면 데이터 일부가 어긋나도 보정 배치를 돌리고 넘어갈 수 있지만, 은행 원장은 다릅니다. 원장은 고객 자산의 법적 기록이고, 회계 감사와 금융감독원 검사의 대상이며, 차변과 대변의 합이 어긋나는 순간 그 날의 일마감 자체가 중단됩니다.

이 글에서는 은행 원장(ledger)을 처음부터 설계한다고 가정하고, 복식부기의 기본 원리부터 데이터 모델, 잔액 계산 전략, 이벤트 소싱, 동시성 제어, 분산 환경의 정합성 문제까지 차례로 다룹니다. 핀테크 스타트업에서 내부 원장을 만들거나, 포인트·선불충전금 같은 유사 원장을 설계하는 분들께도 그대로 적용되는 내용입니다.

참고로 이 글은 시스템 설계 관점의 기술 자료이며, 회계·세무·법률 자문이 아닙니다. 실제 금융업 인허가나 회계 처리는 반드시 전문가와 규정(전자금융감독규정 등)을 확인하셔야 합니다.

원장은 왜 은행의 진실 원천인가

은행의 계정계(코어뱅킹) 시스템에서 원장은 모든 거래의 최종 기록입니다. 다른 모든 데이터 — 화면에 보이는 잔액, 정보계의 분석 데이터, 모바일 앱의 거래내역 — 는 원장에서 파생된 사본일 뿐입니다.

원장이 진실 원천(source of truth)이어야 하는 이유는 세 가지입니다.

1. **법적 증거력**: 분쟁 발생 시 원장 기록이 법적 근거가 됩니다. 따라서 기록은 불변(immutable)이어야 하고, 수정이 아니라 정정 기록 추가로만 변경됩니다.

2. **회계 무결성**: 은행의 재무제표는 원장에서 집계됩니다. 총계정원장(General Ledger)의 차변 합계와 대변 합계는 항상 일치해야 합니다.

3. **감사 추적성**: 누가, 언제, 어떤 거래를, 어떤 근거로 기록했는지 재구성할 수 있어야 합니다.

이 세 가지 요구가 원장 설계의 모든 결정 — 추가 전용(append-only) 구조, 복식부기, 멱등성, 감사 컬럼 — 을 끌어냅니다.

복식부기 기본 — 차변과 대변

복식부기(double-entry bookkeeping)는 모든 거래를 최소 두 개의 계정에 동시에 기록하는 방식입니다. 한쪽은 차변(Debit), 다른 쪽은 대변(Credit)이며, 한 거래 안에서 차변 금액 합계와 대변 금액 합계는 반드시 같아야 합니다.

계정과목은 크게 다섯 분류로 나뉘고, 분류에 따라 증가가 차변인지 대변인지 정해집니다.

| 계정 분류 | 예시 | 증가 시 기록 | 감소 시 기록 |

| --- | --- | --- | --- |

| 자산(Asset) | 현금, 대출채권 | 차변 | 대변 |

| 부채(Liability) | 고객 예금 | 대변 | 차변 |

| 자본(Equity) | 자본금 | 대변 | 차변 |

| 수익(Revenue) | 이자수익, 수수료수익 | 대변 | 차변 |

| 비용(Expense) | 이자비용 | 차변 | 대변 |

여기서 개발자가 자주 혼동하는 포인트가 있습니다. 고객 예금은 은행 입장에서 **부채**입니다. 고객이 맡긴 돈은 언젠가 돌려줘야 할 빚이기 때문입니다. 반대로 대출은 은행 입장에서 **자산**(받을 돈)입니다.

예제로 고객 A가 현금 100만 원을 입금하는 거래를 분개(journal entry)하면 다음과 같습니다.

| 계정과목 | 차변 | 대변 |

| --- | --- | --- |

| 현금 (자산 증가) | 1,000,000원 | |

| 고객예수금 (부채 증가) | | 1,000,000원 |

고객 A가 고객 B에게 30만 원을 이체하는 내부 이체는 이렇게 됩니다.

| 계정과목 | 차변 | 대변 |

| --- | --- | --- |

| 고객예수금-A (부채 감소) | 300,000원 | |

| 고객예수금-B (부채 증가) | | 300,000원 |

대출 이자 5만 원을 수취하는 거래는 다음과 같습니다.

| 계정과목 | 차변 | 대변 |

| --- | --- | --- |

| 고객예수금-A (부채 감소) | 50,000원 | |

| 이자수익 (수익 증가) | | 50,000원 |

모든 분개에서 차변 합계와 대변 합계가 같다는 불변식 — 이를 차대 평형(balanced entry)이라고 부릅니다 — 이 원장 정합성의 핵심입니다. 이 불변식은 애플리케이션 코드, DB 제약, 배치 검증의 세 겹으로 지켜야 합니다.

원장 데이터 모델 — 저널, 포스팅, 밸런스

실무에서 검증된 원장 모델은 세 개의 핵심 테이블로 구성됩니다.

- **journal_entries (분개 헤더)**: 하나의 비즈니스 거래. 거래 ID, 거래 유형, 발생 시각, 멱등성 키를 가집니다.

- **postings (분개 라인)**: 분개를 구성하는 개별 차변·대변 기록. 한 분개에 최소 2건이 속합니다.

- **account_balances (잔액)**: 계정별 현재 잔액의 구체화 뷰(materialized view) 성격의 테이블.

PostgreSQL 기준 스키마 예제입니다.

-- 계정 마스터

CREATE TABLE accounts (

account_id BIGINT PRIMARY KEY,

account_no VARCHAR(20) NOT NULL UNIQUE,

account_type VARCHAR(10) NOT NULL, -- ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE

currency CHAR(3) NOT NULL DEFAULT 'KRW',

status VARCHAR(10) NOT NULL DEFAULT 'ACTIVE',

created_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

-- 분개 헤더 (저널)

CREATE TABLE journal_entries (

entry_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

txn_id UUID NOT NULL UNIQUE, -- 멱등성 키

txn_type VARCHAR(30) NOT NULL, -- DEPOSIT, TRANSFER, INTEREST ...

business_date DATE NOT NULL, -- 기산일(영업일 기준)

posted_at TIMESTAMPTZ NOT NULL DEFAULT now(),

reversal_of BIGINT REFERENCES journal_entries(entry_id),

description TEXT,

created_by VARCHAR(50) NOT NULL

);

-- 분개 라인 (포스팅) — 추가 전용, UPDATE/DELETE 금지

CREATE TABLE postings (

posting_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

entry_id BIGINT NOT NULL REFERENCES journal_entries(entry_id),

account_id BIGINT NOT NULL REFERENCES accounts(account_id),

direction CHAR(1) NOT NULL CHECK (direction IN ('D', 'C')),

amount NUMERIC(19, 4) NOT NULL CHECK (amount > 0),

currency CHAR(3) NOT NULL,

business_date DATE NOT NULL

);

CREATE INDEX idx_postings_account_date

ON postings (account_id, business_date, posting_id);

-- 계정별 잔액 (파생 데이터)

CREATE TABLE account_balances (

account_id BIGINT PRIMARY KEY REFERENCES accounts(account_id),

balance NUMERIC(19, 4) NOT NULL DEFAULT 0,

last_posting_id BIGINT NOT NULL DEFAULT 0,

updated_at TIMESTAMPTZ NOT NULL DEFAULT now()

);

설계 포인트를 짚어보겠습니다.

1. **금액은 NUMERIC(또는 DECIMAL)**: 부동소수점(FLOAT, DOUBLE)은 절대 금지입니다. 0.1 + 0.2가 0.3이 아닌 세계에서 돈을 다룰 수는 없습니다. 통화 최소 단위(원, 센트)의 정수로 저장하는 방식도 널리 쓰입니다.

2. **direction과 amount 분리**: 음수 금액으로 차대를 표현하는 방식보다, 방향 컬럼과 양수 금액으로 분리하는 편이 집계 쿼리와 검증이 명확합니다.

3. **business_date(기산일)와 posted_at(시스템 시각) 분리**: 23시 59분에 들어온 거래가 다음 영업일로 기산되는 경우처럼, 회계상 날짜와 물리적 기록 시각은 다를 수 있습니다.

4. **postings는 추가 전용**: 데이터베이스 권한 수준에서 UPDATE와 DELETE를 차단합니다.

-- 애플리케이션 계정에는 INSERT/SELECT만 허용

REVOKE UPDATE, DELETE ON postings FROM app_user;

GRANT INSERT, SELECT ON postings TO app_user;

잔액 계산 전략 — 합산 vs 실시간 잔액 vs 스냅샷

계정 잔액을 구하는 방법은 크게 세 가지이고, 각각 트레이드오프가 다릅니다.

| 전략 | 방식 | 장점 | 단점 |

| --- | --- | --- | --- |

| 포스팅 합산 | 매번 SUM 쿼리 | 항상 정확, 파생 상태 없음 | 거래 누적 시 느려짐 |

| 실시간 잔액 테이블 | 거래마다 잔액 갱신 | 조회 O(1), 잔액 검증 즉시 가능 | 갱신 경합, 정합성 이중 관리 |

| 스냅샷 + 증분 | 일마감 시점 잔액 저장, 이후 증분만 합산 | 조회 빠르고 검증 가능 | 스냅샷 배치 운영 필요 |

실무에서는 세 가지를 조합합니다. 거래 시점에는 잔액 테이블을 갱신하고(부족 잔액 차단을 위해), 일마감 때 스냅샷을 남기고, 검증 배치가 포스팅 합산과 잔액 테이블을 대조합니다.

스냅샷 기반 잔액 계산 쿼리는 다음과 같은 형태가 됩니다.

-- 어제 마감 스냅샷 + 오늘 증분 = 현재 잔액

SELECT s.balance

+ COALESCE(SUM(

CASE WHEN p.direction = 'C' THEN p.amount

ELSE -p.amount END), 0) AS current_balance

FROM balance_snapshots s

LEFT JOIN postings p

ON p.account_id = s.account_id

AND p.business_date > s.snapshot_date

WHERE s.account_id = 1234567

AND s.snapshot_date = (

SELECT MAX(snapshot_date) FROM balance_snapshots

WHERE account_id = 1234567)

GROUP BY s.balance;

부채 계정(예금)은 대변이 증가이므로 위처럼 C를 더하고 D를 빼며, 자산 계정은 부호가 반대입니다. 계정 분류별 부호 규칙을 한 곳에 모아두지 않으면 반드시 버그가 납니다.

이벤트 소싱과 원장 — 정정은 역분개로

원장은 본질적으로 이벤트 소싱(event sourcing)과 같은 구조입니다. 불변의 추가 전용 저널이 이벤트 스트림이고, 잔액은 이벤트를 폴드(fold)한 파생 상태입니다. 회계가 수백 년 전부터 해온 방식이 그대로 이벤트 소싱입니다.

가장 중요한 원칙은 **기록을 절대 고치지 않는다**는 것입니다. 잘못된 분개는 UPDATE로 수정하는 것이 아니라, 원래 분개를 상쇄하는 역분개(reversal entry)를 추가하고, 올바른 분개를 다시 기록합니다.

def reverse_entry(conn, original_entry_id: int, reason: str, operator: str):

"""원본 분개를 상쇄하는 역분개를 생성한다.

원본의 각 포스팅에 대해 방향(D/C)을 뒤집은 포스팅을 만든다.

원본 데이터는 절대 변경하지 않는다.

"""

with conn.cursor() as cur:

이미 역분개된 분개인지 확인 (이중 역분개 방지)

cur.execute(

"SELECT 1 FROM journal_entries WHERE reversal_of = %s",

(original_entry_id,))

if cur.fetchone():

raise AlreadyReversedError(original_entry_id)

cur.execute(

"""INSERT INTO journal_entries

(txn_id, txn_type, business_date,

reversal_of, description, created_by)

SELECT gen_random_uuid(), 'REVERSAL', CURRENT_DATE,

entry_id, %s, %s

FROM journal_entries WHERE entry_id = %s

RETURNING entry_id""",

(reason, operator, original_entry_id))

reversal_id = cur.fetchone()[0]

방향을 뒤집은 포스팅 복사

cur.execute(

"""INSERT INTO postings

(entry_id, account_id, direction, amount,

currency, business_date)

SELECT %s, account_id,

CASE direction WHEN 'D' THEN 'C' ELSE 'D' END,

amount, currency, CURRENT_DATE

FROM postings WHERE entry_id = %s""",

(reversal_id, original_entry_id))

return reversal_id

역분개 방식의 장점은 분명합니다.

- 원본 기록과 정정 기록이 모두 남아 감사 추적이 완전합니다.

- 잔액 재계산 로직이 단순합니다. 모든 포스팅을 합산하면 항상 현재 상태가 나옵니다.

- "언제 시점의 잔액"을 재구성할 수 있습니다(시점 조회, point-in-time query).

주의할 점은 역분개가 발생한 날짜입니다. 원본이 어제 거래라도 역분개는 오늘 기산일로 기록하는 것이 일반적입니다. 이미 마감된 영업일의 숫자를 바꾸면 보고서와 회계 장부가 소급 변경되기 때문입니다.

동시성 제어 — 핫 어카운트 문제

개별 고객 계좌는 동시 거래가 드물지만, 수수료 수익 계정이나 은행 내부 미결제 계정 같은 곳은 초당 수천 건의 포스팅이 몰립니다. 이런 계정을 핫 어카운트(hot account)라고 부릅니다.

잔액 테이블을 거래마다 갱신하는 구조에서 핫 어카운트는 락 경합의 중심이 됩니다.

[핫 어카운트 락 경합]

거래1 ──┐

거래2 ──┤ 모두 같은 행을 UPDATE account_balances

거래3 ──┼─ 갱신하려고 대기 ──▶ SET balance = balance + ?

... ──┤ WHERE account_id = 9999 (수수료 계정)

거래N ──┘

──▶ 직렬화되어 처리량이 단일 행 갱신 속도로 제한됨

해결 전략은 단계적으로 적용합니다.

1. **잔액 즉시 갱신이 꼭 필요한지 재검토**: 수수료 수익 계정은 한도 검사가 필요 없으므로, 포스팅만 기록하고 잔액은 비동기 집계해도 됩니다. 한도·부족잔액 검사가 필요한 계정만 동기 갱신합니다.

2. **락 순서 고정**: 이체처럼 두 계좌를 갱신할 때는 항상 account_id 오름차순으로 락을 잡아 데드락을 예방합니다.

3. **서브 어카운트 샤딩**: 핫 어카운트를 N개의 서브 계정으로 쪼개고, 포스팅을 해시 분산한 뒤 조회 시 합산합니다.

-- 락 순서 고정 예시: 항상 작은 account_id 먼저

SELECT * FROM account_balances

WHERE account_id IN (1001, 2002)

ORDER BY account_id

FOR UPDATE;

[서브 어카운트 샤딩]

수수료수익 계정 (논리)

├── 수수료수익-00 ◀─ hash(txn_id) % 4 == 0

├── 수수료수익-01 ◀─ hash(txn_id) % 4 == 1

├── 수수료수익-02 ◀─ hash(txn_id) % 4 == 2

└── 수수료수익-03 ◀─ hash(txn_id) % 4 == 3

조회 시: 4개 서브 계정 잔액 SUM

마감 시: 서브 계정을 본 계정으로 집계 분개(선택)

샤딩의 대가는 "논리 계정의 정확한 실시간 잔액"을 단일 행 읽기로 얻을 수 없다는 점입니다. 한도 검사가 필요한 계정에는 적용하기 어렵고, 집계성 내부 계정에 주로 씁니다.

멱등성과 중복 방지 — 거래 ID 설계

금융 시스템에서 재시도는 일상입니다. 네트워크 타임아웃 후 클라이언트가 재시도했을 때 이체가 두 번 실행되면 안 됩니다. 핵심은 **클라이언트가 생성한 거래 ID(멱등성 키)를 저널의 유니크 제약으로 강제**하는 것입니다.

-- txn_id UNIQUE 제약이 중복 기재를 DB 수준에서 차단

INSERT INTO journal_entries (txn_id, txn_type, business_date, created_by)

VALUES ('3fa85f64-5717-4562-b3fc-2c963f66afa6', 'TRANSFER', CURRENT_DATE, 'api')

ON CONFLICT (txn_id) DO NOTHING

RETURNING entry_id;

-- RETURNING이 비어 있으면 이미 처리된 거래 → 기존 결과를 조회해 반환

거래 ID 설계 시 체크할 사항입니다.

- **생성 주체**: 호출자(채널계, 외부 기관)가 생성해야 재시도 간 동일성이 보장됩니다. 서버가 생성하면 멱등성이 깨집니다.

- **범위와 수명**: 기관 코드 + 일자 + 일련번호 조합(금융권 전문에서 흔한 방식)이라면 일자가 바뀔 때 재사용되는지, 유니크 제약 범위에 일자를 포함해야 하는지 확인합니다.

- **응답 재생**: 중복 요청에는 에러가 아니라 **최초 처리 결과와 동일한 응답**을 돌려줘야 합니다. 이를 위해 처리 결과를 거래 ID로 조회 가능하게 저장합니다.

정합성 검증 — 믿되, 검증하라

원장의 불변식은 코드만으로 지켜지지 않습니다. 배포 사고, 수동 데이터 보정, 버그가 언제든 정합성을 깰 수 있으므로 검증을 상시 운영해야 합니다.

**1. 분개 단위 차대 평형 검사** — 모든 분개에서 차변 합계와 대변 합계가 같은지 확인합니다.

SELECT entry_id,

SUM(CASE WHEN direction = 'D' THEN amount ELSE 0 END) AS debit_sum,

SUM(CASE WHEN direction = 'C' THEN amount ELSE 0 END) AS credit_sum

FROM postings

WHERE business_date = CURRENT_DATE

GROUP BY entry_id

HAVING SUM(CASE WHEN direction = 'D' THEN amount ELSE 0 END)

<> SUM(CASE WHEN direction = 'C' THEN amount ELSE 0 END);

-- 결과가 0건이어야 정상

**2. 시산표(trial balance) 검사** — 전체 원장의 차변 총합과 대변 총합이 일치하는지 확인합니다.

**3. 잔액 테이블 대사** — account_balances의 값과 postings 합산 값을 계정별로 비교합니다. 어긋난 계정은 알림을 발생시키고, 자동 보정이 아니라 **원인 조사**가 먼저입니다.

**4. 외부 대사(reconciliation)** — 타행 이체라면 금융결제원 전문 내역과, 카드 매입이라면 카드사 정산 파일과 대조합니다. 내부 정합성과 외부 정합성은 별개의 문제입니다.

검증 배치는 일마감 직전과 직후에 각각 수행하고, 차이가 0이 아니면 마감을 중단하는 게이트로 둡니다.

분산 원장 문제 — 마이크로서비스 시대의 이중 기재

원장 서비스와 결제 서비스가 분리된 마이크로서비스 환경에서는 "결제는 성공했는데 원장 기재가 실패"하는 부분 실패가 발생합니다. 분산 트랜잭션(2PC)은 가용성과 운영 비용 때문에 금융권에서도 점점 기피되고, 사가(saga) 패턴과 보상 거래가 표준 접근이 됐습니다.

[이체 사가 — 정상 흐름과 보상 흐름]

채널 ──▶ 이체 오케스트레이터

├─ 1. 출금 원장 기재 (출금계좌 D / 미결제 C)

│ 성공

├─ 2. 대외 송금 요청 (금융결제원/타행)

│ │

│ ├─ 성공 ──▶ 3. 미결제 해소 분개 (미결제 D / 타행채권 C)

│ │

│ └─ 실패 ──▶ 3'. 보상: 출금 역분개 (미결제 D / 출금계좌 C)

└─ 모든 단계는 멱등 — 같은 txn_id 재시도 시 결과 동일

설계 원칙은 다음과 같습니다.

- **미결제(suspense) 계정을 명시적으로 모델링**합니다. "돈이 떠 있는 상태"를 계정으로 표현하면, 사가의 중간 상태도 차대 평형을 유지합니다.

- **보상은 삭제가 아니라 역분개**입니다. 실패한 흐름도 기록으로 남습니다.

- **타임아웃은 실패가 아닙니다.** 대외 기관 응답 타임아웃 시 결과를 알 수 없으므로, 거래 상태를 미확인(UNKNOWN)으로 두고 대사 또는 거래 결과 조회로 확정한 뒤 보상 여부를 결정합니다. 금융 사고의 단골 원인이 "타임아웃을 실패로 간주하고 보상했는데 원거래도 성공"하는 이중 처리입니다.

회계 시스템 연동 — 계정계에서 총계정원장으로

고객 단위의 거래 원장(보조원장, sub-ledger)과 재무 회계의 총계정원장(GL)은 보통 분리되어 있습니다. 계정계의 수백만 건 거래를 그대로 GL에 넣지 않고, 계정과목·조직·통화 단위로 **집계 분개**를 만들어 일 단위로 GL에 전기(posting)합니다.

[계정계 → 총계정원장 연동]

계정계 원장 (거래 단위, 수백만 건/일)

▼ 일마감 배치: 계정과목 x 부서 x 통화로 GROUP BY

집계 분개 파일 (수천 건/일)

▼ GL 인터페이스 (검증: 차대 평형, 계정과목 매핑 존재)

총계정원장 (재무 보고용)

여기서의 함정은 **매핑 누락**입니다. 신규 상품이나 신규 거래 유형이 추가됐는데 GL 계정과목 매핑이 없으면, 집계 배치가 실패하거나(다행) 매핑 누락분이 조용히 빠집니다(재앙). 매핑 없는 거래 유형을 발견하면 배치를 실패시키는 편이 안전합니다.

감사 추적 — 누가, 언제, 왜

원장 데이터에는 거래 자체 외에 다음 메타데이터가 필요합니다.

- 기록 주체: 채널(모바일/창구/배치), 운영자 ID, 승인자 ID

- 근거: 원거래 참조, 역분개 사유, 관련 전문(message) ID

- 시각: 시스템 시각, 기산일, (필요시) 채널 요청 시각

전자금융거래 기록은 전자금융감독규정에 따라 유형별로 수년 단위의 보존 의무가 있습니다(유형에 따라 1년, 5년 등). 보존 기간 설계는 컴플라이언스 부서와 반드시 함께 결정하고, 파티셔닝과 아카이빙 전략을 처음부터 고려해야 합니다.

흔한 버그 사례

실제로 자주 만나는 원장 버그 유형입니다.

1. **부동소수점 사용**: 이자 계산을 double로 하다가 누적 오차로 일마감 1원 불일치. 금액은 처음부터 끝까지 십진 정밀 타입으로.

2. **부호 규칙 분산**: "자산은 차변이 증가"라는 규칙이 서비스 A와 배치 B에 각각 하드코딩되어 한쪽만 수정됨. 부호 규칙은 단일 모듈로.

3. **재시도 시 신규 거래 ID 발급**: 클라이언트 SDK가 재시도마다 UUID를 새로 생성해 멱등성이 무력화됨.

4. **역분개의 역분개**: 동일 분개에 역분개가 두 번 걸려 잔액이 반대 방향으로 틀어짐. 역분개 존재 여부를 유니크 제약으로 차단.

5. **마감 후 소급 기재**: 마감된 영업일에 포스팅이 들어가 보고서와 원장이 불일치. business_date에 대한 마감 가드 필요.

6. **시간대 문제**: UTC와 KST가 섞여 자정 부근 거래의 기산일이 어긋남. 기산일 결정 로직을 한 곳으로.

설계 체크리스트

- [ ] 금액 타입은 십진 정밀(NUMERIC/DECIMAL 또는 최소 단위 정수)인가

- [ ] postings는 추가 전용이며 DB 권한으로 UPDATE/DELETE가 차단되어 있는가

- [ ] 모든 분개가 차대 평형인지 애플리케이션과 배치 양쪽에서 검증하는가

- [ ] 거래 ID는 호출자가 생성하고 유니크 제약으로 중복이 차단되는가

- [ ] 중복 요청에 최초 결과와 동일한 응답을 재생하는가

- [ ] 정정은 역분개로만 하며, 이중 역분개가 차단되는가

- [ ] 기산일과 시스템 시각이 분리되어 있고 마감일 소급 기재가 차단되는가

- [ ] 핫 어카운트를 식별했고 동기 잔액 갱신이 필요한 계정을 최소화했는가

- [ ] 잔액 테이블과 포스팅 합산의 대사 배치가 운영 중인가

- [ ] 미결제 계정으로 분산 거래의 중간 상태를 표현하는가

- [ ] 타임아웃을 미확인 상태로 다루고 대사로 확정하는가

- [ ] GL 매핑 누락 시 배치가 실패하도록 되어 있는가

- [ ] 감사 메타데이터(주체, 근거, 시각)와 보존 기간이 정의되어 있는가

마치며

원장 설계의 본질은 화려한 기술이 아니라 **불변식을 끝까지 지키는 집요함**입니다. 복식부기라는 수백 년 된 불변식, 추가 전용이라는 단순한 규칙, 멱등성이라는 분산 시스템의 기본기 — 이 세 가지를 코드, 스키마, 배치 검증의 세 겹으로 강제하면 1원의 정합성은 지켜집니다. 반대로 어느 한 겹이라도 "나중에 하지"라고 미루면, 그 비용은 반드시 새벽의 마감 장애로 돌아옵니다.

다음 글에서는 이 원장 위에서 돌아가는 금융권 배치와 일마감(EOD) 아키텍처를 다룹니다.

참고 자료

- ISO 20022 공식 사이트: https://www.iso20022.org/

- BIS (국제결제은행): https://www.bis.org/

- 바젤은행감독위원회(BCBS): https://www.bis.org/bcbs/

- SWIFT 표준 문서: https://www.swift.com/standards

- 금융결제원: https://www.kftc.or.kr/

- 금융감독원: https://www.fss.or.kr/

- Martin Fowler — Event Sourcing: https://martinfowler.com/eaaDev/EventSourcing.html

- Martin Fowler — Accounting Patterns: https://martinfowler.com/eaaDev/AccountingNarrative.html

- PostgreSQL 공식 문서 (Numeric Types): https://www.postgresql.org/docs/current/datatype-numeric.html

- PostgreSQL 공식 문서 (Explicit Locking): https://www.postgresql.org/docs/current/explicit-locking.html

- IFRS 재단: https://www.ifrs.org/

현재 단락 (1/267)

은행 시스템을 처음 접하는 개발자가 가장 놀라는 지점은 "잔액이 1원이라도 안 맞으면 마감이 안 된다"는 사실입니다. 일반적인 웹 서비스라면 데이터 일부가 어긋나도 보정 배치를 돌...

작성 글자: 0원문 글자: 10,665작성 단락: 0/267