fos-blog/study
01 / 홈02 / 카테고리03 / 시리즈
01 / 홈02 / 카테고리03 / 시리즈

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • agent 페이지로 이동
    • langgraph 페이지로 이동
    • 사람용 CLI와 AI 에이전트용 CLI는 설계가 다르다
    • agents.md
    • Claude Code 메모리: CLAUDE.md와 .claude/rules를 규칙으로 쓰는 법
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • Docling — IBM Research 의 문서 파싱 toolkit 상세 정리
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
    • 온톨로지에서 코딩 에이전트 컨텍스트까지 — 클래스·관계 설계와 그래프 평가
    • OpenClaw는 context와 memory를 어떻게 관리하나 — 나만의 에이전트를 구성하는 법
    • OpenClaw vs Hermes Agent — 갈아탈까 고민하며 정리한 비교
  • ai 페이지로 이동
    • agent 페이지로 이동
    • [초안] AI 제품 백엔드 안정성 — 지연·비용·권한·관측·도구 실패·폴백/재시도/사람 에스컬레이션
    • [초안] LLM 평가 프레임워크: 골든셋, 회귀 테스트, LLM-as-a-judge, 사람 피드백 루프
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • 시니어 백엔드를 위한 API 설계 실전 스터디 팩
    • API 버저닝과 하위 호환성: 모바일·외부 컨슈머까지 안전하게 진화시키기
    • 캐시 설계 전략 총정리
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 도메인 모델링: 주문·재고·노출의 세 축을 분리해서 설계하기
    • 커머스 주문 상태와 데이터 정합성 기본기
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] Event Sourcing과 CQRS — 상태가 아니라 변화를 저장한다는 발상
    • [학습중] 금융 거래 취소·정정·대사·일마감 운영
    • [학습중] 금융 거래 상태와 원장 설계
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리
    • [초안] F&B 주문/매장/픽업 상태머신 설계
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 대규모 회원과 메가 프로모션을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [학습중] 모듈러 모놀리스에서 MSA로 점진 전환하는 실습
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • milvus 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • qdrant 페이지로 이동
    • redis 페이지로 이동
    • vespa 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • [초안] DB Connection Pool Saturation과 Thread Pool 격리
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 한국어 형태소 분석기 Nori vs Lindera — OpenSearch에서 Milvus로 갈 때 어휘 검색은 유지되나
    • 벡터 DB 5종, 아키텍처는 어떻게 다른가
    • 벡터 DB 5종을 실제로 벤치마크했다 — 같은 recall에서 QPS는 얼마나 갈리나
    • 벡터 DB 어떻게 고를까 — OpenSearch · Milvus · Qdrant · Vespa · pgvector 비교
    • 벡터 DB를 실제로 도입한 사례 — 빅테크 프로덕션
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • [초안] 운영 데이터 정합성 장애 대응 — 결제 취소 누락과 중복 적재 런북
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
    • [초안] 시니어 백엔드를 위한 SLO와 Error Budget 기반 장애 대응
  • http 페이지로 이동
    • HTTP Connection Pool
    • HTTPS는 어떻게 안전한가 — TLS, 인증서, 그리고 termination
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 캐시 정합성 / 12일 AI 웹툰 MVP
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • testing 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • [학습중] Kafka 파티션·리밸런스·컨슈머 지연 운영
    • 메시지 전송 신뢰성
    • [초안] Spring Kafka 컨슈머 오프셋 커밋과 트랜잭션 정렬: AckMode, manual ack, 멱등 처리
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • mlops 페이지로 이동
    • llm-serving 페이지로 이동
    • serving-frameworks 페이지로 이동
    • Python CUDA 버전 생태계 — nvidia-smi, nvcc, pip, conda가 다 다른 버전을 말하는 이유
    • GPU 컨테이너의 CUDA 버전 호환성 — nvidia-smi부터 이미지 다이어트까지
    • Kubernetes GPU 노드에서 /run tmpfs가 꽉 차서 Pod가 안 뜰 때
    • GPU·CUDA·MPS 기초 — 자바 백엔드 개발자가 처음 만나는 그림
    • Multi-process GPU 워크로드 — 자바 ThreadPool 사용자가 만나는 모델 차이
    • ML 서비스 성능 분석 워크플로 — 자바 백엔드 트러블슈팅과 다른 점
    • 한 GPU 를 여러 프로세스가 나눠 쓰기 — Time-Slicing 과 MPS
  • network 페이지로 이동
    • Connection reset by peer는 누가 보낸 걸까 — 리버스 프록시 홉마다 TCP 연결은 따로 논다
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • python 페이지로 이동
    • Python async/await — CompletableFuture·Reactor 와 다른 점, 그리고 blocking I/O 함정
    • Python 의존성 관리 — Java Maven/Gradle 사용자가 만나는 첫 충격
    • FastAPI 기초 — Spring Boot 사용자가 빠르게 익히는 법
    • Java 개발자를 위한 Python 심화 — OOP·데코레이터·컨텍스트 매니저
    • PyTorch 기초 — 텐서, 디바이스, 그리고 모델 로딩이 무거운 이유
    • Java 개발자를 위한 Python 문법 핵심
    • ThreadLocal 에서 contextvars 로 — Python 의 요청 컨텍스트 전파
    • OCR 동작 원리 — Layout · Text · Post-process 3단계
    • Python 서버의 RSS 가 안 줄어드는 이유 — gc.collect 의 한계와 malloc_trim
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • resume 페이지로 이동
    • [초안] 김병태 경력기술서
    • [초안] 김병태 포트폴리오
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
    • [초안] Spring Security 6.x OAuth2 + JWT 상용 인증 설계 — Grant 선택, Resource Server, Refresh Rotation, 로그아웃
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • thinking 페이지로 이동
    • 좋은 일을 넘어 중요한 일을 하는 법
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • Glossary↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/architecture/[학습중] 금융 거래 상태와 원장 설계
architecturedb

[학습중] 금융 거래 상태와 원장 설계

이 문서는 수신·여신·지급결제 서비스에서 돈의 상태가 어떻게 변하고, 그 변화를 원장에 어떻게 남기는지 공부하기 위해 만들었다. 학습 목표는 거래 상태와 잔액을 구분하고, 이중 기록과 불변 조건으로 오류를 탐지하며, 재시도에도 같은 거래를 한 번만 반영하는 설계를 설명하는 것이다. 완료 기준은 간단한 계좌이체를 상태 기계와 복식 원장으로 구현하고, 중복 요...

2026.07.21·8 min read·31 views

이 문서는 수신·여신·지급결제 서비스에서 돈의 상태가 어떻게 변하고, 그 변화를 원장에 어떻게 남기는지 공부하기 위해 만들었다. 학습 목표는 거래 상태와 잔액을 구분하고, 이중 기록과 불변 조건으로 오류를 탐지하며, 재시도에도 같은 거래를 한 번만 반영하는 설계를 설명하는 것이다. 완료 기준은 간단한 계좌이체를 상태 기계와 복식 원장으로 구현하고, 중복 요청·타임아웃·부분 실패 테스트를 통과시키는 것이다.

왜 금융 거래는 일반 상태 변경과 다른가

게시글의 상태를 DRAFT에서 PUBLISHED로 바꾸는 작업은 실패하면 다시 시도해도 대체로 안전하다. 돈을 옮기는 작업은 같은 명령이 두 번 적용되면 곧바로 손실이 생긴다. 응답이 유실됐을 때 클라이언트는 실패로 보지만 서버에서는 이미 원장 반영이 끝났을 수도 있다.

금융 백엔드는 다음 질문에 항상 답할 수 있어야 한다.

  • 이 거래는 지금 어느 단계에 있는가.
  • 고객이 사용할 수 있는 금액은 얼마인가.
  • 실제로 확정된 금액은 얼마인가.
  • 동일 요청이 다시 들어오면 이전 결과를 찾을 수 있는가.
  • 잔액이 잘못됐다면 어떤 기록으로 재구성할 수 있는가.

핵심은 현재 잔액 한 칸만 믿지 않는 것이다. 잔액은 빠른 조회를 위한 파생 상태이고, 원장 항목이 금액 이동의 근거가 되어야 한다.

수신·여신·지급결제의 역할

수신은 고객으로부터 자금을 받아 계좌와 예금 상품으로 관리하는 영역이다. 입금, 출금, 이자 지급, 예금 만기, 지급 정지가 대표 흐름이다.

여신은 고객에게 자금을 빌려주고 회수하는 영역이다. 대출 실행, 원금 상환, 이자 발생, 연체, 중도 상환이 대표 흐름이다.

지급결제는 한 주체의 지급 지시를 다른 주체의 자금 수취로 연결하는 영역이다. 계좌이체, 카드 승인, 자동이체, 간편결제 충전과 인출이 이 범주에 들어간다.

세 영역은 상품 규칙이 다르지만 공통된 기술 문제를 가진다.

  • 거래 명령과 회계 반영 사이의 상태 전이를 관리한다.
  • 가용 잔액과 장부 잔액을 구분한다.
  • 외부 기관과 상태가 어긋날 가능성을 전제로 한다.
  • 이미 확정된 기록을 덮어쓰기보다 반대 기록으로 정정한다.

수신에서는 고객이 맡긴 돈을 언제 인출 가능한 상태로 볼지가 중요하다. 입금 전문을 받았다고 곧바로 가용 잔액을 늘릴지, 외부 결제망의 확정 이후에 늘릴지에 따라 상태가 달라진다. 여신에서는 대출 실행 원금, 아직 납부하지 않은 이자, 연체 금액을 같은 잔액으로 뭉개지 않는다. 각 금액의 발생 원인과 상환 순서를 별도 계정 또는 원장 속성으로 표현한다. 지급결제에서는 고객의 지급 지시, 자금 예약, 외부 전송, 최종 정산이 서로 다른 시점에 일어날 수 있다. 따라서 API 한 번의 성공 여부를 거래 전체의 완료 여부로 간주하지 않는다.

이 차이를 모델에 반영하면 상품별 세부 규칙이 달라도 공통된 처리 골격을 재사용할 수 있다. 명령을 검증하고, 자금을 예약하고, 원장에 반영하고, 외부 결과를 확인하고, 대사로 누락을 찾는 순서다. 공통 골격과 상품 정책을 분리하면 새로운 상품이 생겼을 때 원장 정합성 규칙을 다시 구현하는 위험도 줄어든다.

거래 상태와 원장 상태를 분리한다

거래 상태는 업무 절차의 진행 상황을 나타낸다. 원장 상태는 금액 변동이 회계적으로 반영됐는지를 나타낸다.

계좌이체의 단순화된 상태는 다음과 같이 표현할 수 있다.

text
RECEIVED -> VALIDATED -> RESERVED -> POSTED -> COMPLETED
                |            |          |
                v            v          v
             REJECTED     RELEASED   REVERSAL_REQUIRED

RESERVED는 출금 계좌의 가용 잔액을 먼저 줄여 동시 출금을 막은 상태다. POSTED는 차변과 대변 원장 항목이 같은 거래 묶음으로 확정된 상태다. COMPLETED는 후속 알림이나 외부 결과 전달까지 끝난 업무 상태다.

원장 반영은 성공했는데 알림이 실패했다고 원장을 롤백해서는 안 된다. 알림은 재시도하고, 거래의 금전적 결과는 유지해야 한다.

장부 잔액과 가용 잔액

장부 잔액(ledger balance)은 확정된 원장 항목을 반영한 잔액이다. 가용 잔액(available balance)은 지금 고객이 추가로 사용할 수 있는 금액이다.

출금 요청이 승인 대기 중이면 장부 잔액은 그대로여도 가용 잔액은 줄어들 수 있다. 이 차이를 표현하지 않으면 동시에 들어온 두 출금이 모두 잔액 검사를 통과할 수 있다.

text
장부 잔액 100,000원
출금 예약 30,000원
가용 잔액 70,000원

예약이 확정되면 장부 잔액이 70,000원으로 바뀌고 예약 금액은 사라진다. 예약이 취소되면 장부 잔액은 그대로이고 가용 잔액만 다시 100,000원이 된다.

복식 원장의 최소 모델

복식 원장은 한 거래가 둘 이상의 계정에 동일한 총액으로 반영되도록 만든다. 단순 계좌이체에서는 출금 계정에서 10,000원을 빼고 입금 계정에 10,000원을 더한다.

원장 구현에서 중요한 불변 조건은 다음과 같다.

  • 한 거래 묶음의 차변 합과 대변 합이 같다.
  • 원장 항목은 확정 후 수정하지 않는다.
  • 모든 항목은 원래 업무 거래 식별자를 가진다.
  • 같은 멱등성 키는 하나의 거래 결과에만 연결된다.
  • 잔액은 원장 항목으로 다시 계산할 수 있다.
sql
CREATE TABLE ledger_transaction (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    transaction_key VARCHAR(80) NOT NULL,
    transaction_type VARCHAR(40) NOT NULL,
    status VARCHAR(20) NOT NULL,
    created_at TIMESTAMP(6) NOT NULL,
    UNIQUE KEY uk_ledger_transaction_key (transaction_key)
);
 
CREATE TABLE ledger_entry (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    transaction_id BIGINT NOT NULL,
    account_id BIGINT NOT NULL,
    direction VARCHAR(10) NOT NULL,
    amount DECIMAL(19, 2) NOT NULL,
    currency CHAR(3) NOT NULL,
    created_at TIMESTAMP(6) NOT NULL,
    CONSTRAINT fk_entry_transaction
        FOREIGN KEY (transaction_id) REFERENCES ledger_transaction(id),
    CONSTRAINT ck_entry_amount CHECK (amount > 0)
);

direction을 부호 있는 금액 하나로 표현할 수도 있다. 어느 방식을 택하든 거래 단위 합계가 0이라는 불변 조건을 코드와 검증 쿼리로 보장해야 한다.

sql
SELECT transaction_id,
       SUM(CASE WHEN direction = 'DEBIT' THEN amount ELSE -amount END) AS difference
FROM ledger_entry
GROUP BY transaction_id
HAVING difference <> 0;

이 쿼리 결과는 항상 비어 있어야 한다. 결과가 한 건이라도 나오면 단순 로그 경고가 아니라 금전 정합성 사고 후보로 다뤄야 한다.

잔액 스냅샷은 원장을 대체하지 않는다

매 요청마다 전체 원장을 합산하면 조회 비용이 계속 증가한다. 실무에서는 계좌별 잔액 스냅샷을 유지하고 원장과 같은 트랜잭션에서 갱신한다.

sql
CREATE TABLE account_balance (
    account_id BIGINT PRIMARY KEY,
    ledger_balance DECIMAL(19, 2) NOT NULL,
    reserved_amount DECIMAL(19, 2) NOT NULL,
    version BIGINT NOT NULL
);

스냅샷은 성능을 위한 캐시와 비슷하지만 일반 캐시보다 강한 정합성 요구를 가진다. 원장과 잔액이 어긋나면 원장을 기준으로 잔액을 재구축할 수 있어야 한다.

멱등성과 동시성 제어

멱등성 키는 같은 업무 요청을 시간적으로 연결한다. 행 잠금이나 낙관적 잠금은 동시에 실행되는 요청의 충돌을 제어한다. 둘은 대체 관계가 아니다.

java
@Transactional
public TransferResult transfer(TransferCommand command) {
    return transactionRepository.findByTransactionKey(command.transactionKey())
        .map(TransferResult::from)
        .orElseGet(() -> executeNewTransfer(command));
}
 
private TransferResult executeNewTransfer(TransferCommand command) {
    AccountBalance source = balanceRepository.findForUpdate(command.sourceAccountId())
        .orElseThrow();
 
    if (source.availableBalance().compareTo(command.amount()) < 0) {
        throw new InsufficientBalanceException();
    }
 
    LedgerTransaction transaction = ledgerWriter.postTransfer(command);
    balanceUpdater.apply(transaction);
    return TransferResult.from(transaction);
}

조회 후 삽입만으로 멱등성을 보장하려 하면 두 요청이 동시에 없음을 확인할 수 있다. DB의 고유 제약을 최종 방어선으로 두고 중복 키 예외가 발생하면 기존 결과를 다시 조회해야 한다.

나쁜 설계와 개선된 설계

잔액만 직접 수정한다

java
source.decrease(amount);
target.increase(amount);

이 코드는 결과 잔액은 남기지만 왜 변했는지 재구성하기 어렵다. 중간 실패가 발생하면 어느 계정까지 반영됐는지 판단할 근거도 약하다.

개선된 구조는 거래 묶음과 원장 항목을 먼저 정의하고, 같은 DB 트랜잭션에서 잔액 스냅샷을 갱신한다.

확정 기록을 UPDATE로 고친다

과거 원장 금액을 직접 수정하면 감사 추적과 시점별 잔액 재현이 깨진다. 잘못된 기록은 반대 방향의 정정 거래를 추가하고 원거래 식별자를 연결한다.

모든 후속 작업을 한 트랜잭션에 넣는다

외부 알림과 메시지 발행까지 DB 트랜잭션 안에서 기다리면 잠금과 커넥션 점유 시간이 길어진다. 금전 반영과 Outbox 저장까지만 같은 트랜잭션에 두고 외부 전달은 별도 처리한다.

관련 패턴은 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가에서 더 깊게 다룬다.

로컬 실습

MySQL 하나로 작은 계좌이체 원장을 구현한다.

bash
docker run --name ledger-mysql \
  -e MYSQL_ROOT_PASSWORD=root \
  -e MYSQL_DATABASE=ledger_lab \
  -p 3307:3306 \
  -d mysql:8.4

실습은 다음 순서로 진행한다.

  • 계좌 두 개와 초기 입금 원장 항목을 만든다.
  • 동일한 transaction_key로 계좌이체를 동시에 두 번 호출한다.
  • 거래 행과 원장 항목이 한 묶음만 생성되는지 확인한다.
  • 원장 저장 직후 예외를 발생시켜 잔액까지 함께 롤백되는지 확인한다.
  • Outbox 발행 전에 프로세스를 종료하고 재시작 후 다시 발행되는지 확인한다.
  • 원장을 합산한 값과 account_balance를 비교하는 검증 쿼리를 실행한다.

성공 기준은 단순히 API가 200을 반환하는 것이 아니다. 중복 호출과 장애 주입 뒤에도 거래별 합계가 0이고 잔액 스냅샷이 원장 합계와 같아야 한다.

설명할 때의 답변 구조

금융 거래 정합성을 설명할 때는 제품 이름보다 불변 조건에서 시작한다.

거래 상태와 금액 반영 상태를 분리하고, 금액 이동은 수정 가능한 잔액 한 칸이 아니라 append-only 원장 항목으로 남깁니다. 같은 거래의 차변과 대변 합이 일치하도록 검증하고, 계좌 잔액은 조회 성능을 위한 스냅샷으로 관리하되 원장에서 재구축할 수 있게 합니다. 재시도에는 멱등성 키와 DB 고유 제약을 사용하고, 동시 출금에는 행 잠금이나 낙관적 잠금을 함께 적용합니다. 외부 메시지는 같은 트랜잭션에 Outbox를 저장해 이중 쓰기 문제를 줄입니다.

후속 질문에는 다음 근거를 붙인다.

  • 가용 잔액과 장부 잔액이 왜 다른가.
  • 멱등성 키와 잠금이 왜 둘 다 필요한가.
  • 원장과 잔액이 어긋났을 때 무엇을 기준으로 복구하는가.
  • 외부 시스템의 성공 여부가 불명확할 때 어떤 상태를 두는가.

학습 완료 체크리스트

  • 수신·여신·지급결제의 공통 상태 모델을 설명할 수 있다.
  • 거래 상태와 원장 반영 상태를 분리할 수 있다.
  • 장부 잔액과 가용 잔액의 차이를 예로 설명할 수 있다.
  • 차변과 대변 합계 불변 조건을 SQL로 검증할 수 있다.
  • 잔액 스냅샷을 원장에서 재구축할 수 있다.
  • 중복 요청과 동시 요청을 서로 다른 문제로 다룰 수 있다.
  • 확정 원장을 수정하지 않고 정정 거래를 추가해야 하는 이유를 설명할 수 있다.
  • 실패 주입 테스트로 원장과 잔액의 원자성을 검증했다.

참고 자료

  • SAP Help Portal — Universal Journal
  • MySQL 8.4 Reference Manual — InnoDB Transaction Model
  • MySQL 8.4 Reference Manual — Locking Reads
  • AWS Prescriptive Guidance — Transactional Outbox Pattern
on this page
  • 01왜 금융 거래는 일반 상태 변경과 다른가
  • 02수신·여신·지급결제의 역할
  • 03거래 상태와 원장 상태를 분리한다
  • 04장부 잔액과 가용 잔액
  • 05복식 원장의 최소 모델
  • 06잔액 스냅샷은 원장을 대체하지 않는다
  • 07멱등성과 동시성 제어
  • 08나쁜 설계와 개선된 설계
  • 잔액만 직접 수정한다
  • 확정 기록을 UPDATE로 고친다
  • 모든 후속 작업을 한 트랜잭션에 넣는다
  • 09로컬 실습
  • 10설명할 때의 답변 구조
  • 11학습 완료 체크리스트
  • 12참고 자료
tags
#학습중#카카오뱅크#금융도메인#수신#여신#지급결제#원장#정합성

이런 글도

  • [학습중] 모듈러 모놀리스에서 MSA로 점진 전환하는 실습
    이 문서는 대규모 모놀리스를 한 번에 재작성하지 않고 경계를 세운 뒤 작은 서비스부터 분리하는 방법을 공부하기 위해 만들었다. 학습 목표는 코드 모듈화, 데이터 소유권, 호출 전환, 관찰과 롤백을 하나의 마이그레이션 흐름으로 설명하는 것이다. 완료 기준은 작은 Spring Boot 모놀리스에서 한 모듈을 독립 서비스로 추출하고, 기존 경로와 신규 경로의 결...
    🏗️ architecture
    architecture
    2026.07.21
  • [학습중] 금융 거래 취소·정정·대사·일마감 운영
    이 문서는 정상 거래보다 더 자주 운영자의 판단이 필요한 취소·정정·대사·일마감을 공부하기 위해 만들었다. 학습 목표는 확정 거래를 안전하게 되돌리는 방법, 내부 기록과 외부 결과의 차이를 찾는 방법, 영업일 단위로 장부를 닫는 방법을 하나의 운영 흐름으로 연결하는 것이다. 완료 기준은 불일치 데이터를 의도적으로 만들고, 대사 배치가 이를 분류하며, 정정...
    🏗️ architecture
    architecture
    2026.07.21
  • [초안] Event Sourcing과 CQRS — 상태가 아니라 변화를 저장한다는 발상
    이 문서의 목표는 두 가지다. 하나, "현재 상태를 덮어쓰는" 일반적인 CRUD 모델과 "일어난 사건을 append-only로 쌓는" Event Sourcing이 어떻게 다른지 감을 잡는 것. 둘, Event Sourcing과 자주 한 묶음으로 거론되는 CQRS가 사실은 독립된 패턴이며, 언제 함께 쓰고 언제 따로 떼어야 하는지 판단 기준을 세우는 것. 결...
    🏗️ architecture
    architecture
    2026.06.16
  • [초안] Spring Batch vs Event-Driven — 같은 비동기처럼 보이지만 전혀 다른 두 패러다임
    > 관련 문서: Outbox / Inbox Pattern 심화, 분산 트랜잭션과 Outbox 패턴. 본 문서는 두 처리 패러다임의 선택 기준과 trade-off에 집중하고, 위 두 문서는 이벤트 발행의 정합성 메커니즘에 집중한다. 백엔드를 4\5년차 이상 다루다 보면 어느 시점에서 "이건 동기로 처리하기 어렵다"는 결론에 도달한다. 사용자가 결제 버튼을 누...
    🏗️ architecture
    architecture
    2026.05.16

댓글 (0)