현재 필요한 데이터 처리
- 모든 유저의 잔액
- Front End에서 조회( SQL로 한번에 가능 )
- 특정 유저의 잔액
- Front End에서 생성( SQL로 한번에 가능 )
- 계좌 간 송금( 같은 은행 내, 타행 이체 )
- RDG( Random Data Generator )로 생성
- Front End에서 생성
문제는 "계좌 간 송금"이다.
DB를 쓰면 Transaction내에서 원자성을 유지해준다. 여러가지의 이유로 Client 쪽에서 중복으로 발생, 동시 발생의 이유로 계좌이체에서 "멱등키(Idempotent-Key)"를 이용한다.
쉽게 말하면 하나의 작업에 대해 여러번 요청이되어도 한번만 처리되는 루틴이다.
Flask에서 멱등키를 제어하여 DB쪽으로 넘겨줘야하는 상황이다. 이러한 작업은 처음해보는 것이라 조금 정리를 한 후 진행할 예정이다.
정리
기술적인 내용을 정리하기 어떻게 처리하는지, 어떤 구조를 가져야하는지 정리한다.
현재 계좌 간 송금을 할 때 일관성과 안정성을 보장해야한다. 돈에 관련된 것이라 굉장히 중요하다. 송금시 DB는 여러개의 테이블로 이것을 보장한다.
테이블 개념적 내용
1. 계좌 테이블
2. 거래전표 테이블
- 이체시 내역을 저장한다. 이것이 insert 되었다고 이체가 일어난 것이 아니다. 실제 이체 전에 어떤 내역이 일어날지 미리 저장해두는 테이블로 생각하면 된다.
3. 원장 테이블
- 모든 계좌의 거래 내역을 기록하는 테이블이다. 같은 은행에서 계좌이체가 일어나면 2개의 데이터가 insert 된다.
4. 외부 송금 테이블
- 외부송금시, 같은 DB가 아니니 많은 이유로 송금이 실패할 수 있다. 그래서 내가 송금을 할 때 보낸 금액을 hold 해두기 위한 테이블이다.
멱등키란
멱등키(Idempotency Key)는 한마디로 같은 요청이 여러 번 들어와도 결과는 한 번만 나게 하는 식별자로서 클라이언트가 직접 요청에 붙여 보내는 고유 키
현재 이체 프로세스는 RDG.py와 FE에서 이루어질 것이다. 각 프로그램에서 이체를 실행시키 전에 멱등키를 발급한다.
FE : crypto.randomUUID()
BE : uuid.uuid4()
함수를 이용한다. 이것으로 중복된 처리를 일관되고 문제없게 처리한다. 즉 하나의 흐름(하나의 이체)에 대해서 하나의 멱등키를 이용해 중복처리를 막아준다.
uuid는 랜덤값으로 겹치는 값이 나올 확률은 0에 가깝다.
테이블 상세 설계
accounts Table(계좌)
account_id: 계좌 번호
owner_id: 고객 식별자
currency: 통화(KRW 등) <- KRW만 관리할거라 제외
status: ACTIVE/FROZEN/CLOSED
balance: “기장까지 반영된” 잔액
hold_amount: 보류된 금액(외부송금 대기분)
available = balance − hold_amount (가용잔액; 뷰로 보거나 계산해서 씀) <- 사용할 때 계산식으로 유도해서 사용할 예정
transactions Table(거래 전표, 1줄로 발생)
txn_id: 거래 번호, PK
type: INTERNAL(내부이체) / OUT(외부로 보냄) / IN(외부에서 들어옴)
status: PENDING/POSTED/SETTLED/REVERSED
- PENDING : 생성되었으나 확정은 아직 x
- POSTED(내부) : 원장 기장 완료
- SETTLED(외부) : 정산 확정
- REVERSED : 실패/취소 등의 상태
- 추후 각 상황에 따라 숫자코드로 부여할 예정
src_account_id / dst_account_id: 출금/입금 계좌(내부이체면 둘 다 채움)
amount: 금액( 입/출금 다 양수로 관리 )
idempotency_key: 멱등키(중복 기장 방지)
created_at/updated_at
ledger_entries Table( 원장 = 기장 라인들, 실제 돈이 입출이 되었으면 적는다. )
entry_id: 원장 라인 번호
txn_id: 전표 번호
account_id: 계좌번호
amount: 증감 금액(입금은 양수, 출금은 음수)
created_at/updated_at
규칙: 전표 한 건 ↔ 원장 최소 2줄(이중부기).
같은 txn_id의 amount 합계는 항상 0이 되어야 함.
holds Table( 외부송금 보류 전용, 내부이체에는 안 씀 )
hold_id
account_id : 상대 계좌
amount
status: OPEN(대기)/CAPTURED(확정)/RELEASED(해제)
idempotency_key
created_at : 보류가 걸린 시점
각 DBMS의 테이블은 위와 같이 설계할 예정이다.
A 계좌 -> B 계좌로 10,000원 송금
1. 내부이체
1.1 성공시
- 요청 시 데이터 확인
- 전표 생성
- 전표(transactions) 테이블에 status : PENDING 로 데이터 생성
- 여기서 idempotency_key의 값을 넣어준다. <- FE에선 React에서 ,RDG에선 Flask에서 생성해서 넣는다.
- 계좌 락을 건다.
- 이체를 수행하는 중 이 계좌를 다른 사람이 건들이면 문제가 생길 수 있다.
- 원장 기장
- A : -10,000 , B : +10,000
- 잔액 반영
- 전표 반영
1.2 실패시
- 계좌가 없거나 금액이 없을 경우 전표 미생성 후 바로 에러출력
- 동시요청 및 중복요청 -> 멱등키가 하나발급이 되어 멱등키가 중복 시 첫건에 대해서만 성공
2. 외부이체
출금은행 프로세스
- 요청 시 데이터 확인
- 계좌 존재/상태 확인, 금액 > 0, 가용잔액 ≥ 금액 확인
- (선행) FE/RDG가 생성한 Idempotency-Key(K_out) 헤더 유입 여부 확인
- 전표 생성
- 전표(transactions) 테이블에 type: OUT, status: PENDING 으로 생성
- idempotency_key = K_out 저장(멱등 선점)
- createId(list_id_out) 발급(확정 단계의 멱등 기준)
- holds 데이터 생성
- holds 레코드 status: OPEN, idempotency_key: K_out 으로 생성
- accounts.hold_amount += amount → 가용잔액 감소
- 이 단계에서 원장 기장은 대기(없음)
- 멱등키 발생
- FE: crypto.randomUUID() / RDG: uuid.uuid4() 로 요청 전 생성된 K_out을 사용
- 같은 K_out 재요청 시: 기존 결과 재사용(중복 기장 없음)
- 같은 K_out + 다른 바디(금액/계좌 변경) 시: 409/422 에러
- 외부 지시/내부 호출
- 우리 환경에선 수신은행 프로세스의 “수금 전표 생성”을 내부 호출로 대체
- X-Correlation-Id 등 추적 값은 선택
- 확정(성공)
- holds: OPEN → CAPTURED
- 원장 기장(권장 2줄): 출금계좌 −금액, 출금은행_정산계정 +금액
- transactions.status = SETTLED
- 확정 멱등 기준: list_id_out(같은 ID로 여러 번 와도 1회만 전이)
- 실패(거절/시간초과)
- holds: OPEN → RELEASED
- 원장 기장 없음
- transactions.status = REVERSED
- 같은 list_id_out 재호출 시 상태 재전이 없음(멱등)
수신은행 프로세스
- 수금 전표 생성
- 전표(transactions) 테이블에 type: IN, status: PENDING 으로 생성
- idempotency_key = K_in 저장(출금 쪽과 별도 키)
- createId(list_id_in) 발급
- 확정(성공)
- 원장 기장(권장 2줄): 수신은행_정산계정 −금액, 입금계좌 +금액
- transactions.status = SETTLED
- 확정 멱등 기준: list_id_in
- 실패
- 멱등키 처리
- 같은 K_in 재요청 시 기존 결과 재사용
- 같은 K_in + 다른 바디면 409/422 에러