본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 21. 00:40

AI에 거의 전적으로 맡겨 '차세대 은행 결제 협조층'을 만들어 본 이야기: 레거시 계정계를 유지하며 프로그래머빌리티를 구현하다

요약

레거시 은행 계정계를 유지하면서도 프로그래머빌리티를 확보하기 위해, AI를 활용하여 차세대 결제 협조층(Zenith Coordinator)의 참조 구현체를 개발한 사례를 소개합니다. Cloudflare Workers와 D1을 기반으로 설계되었으며, 결제 속도와 리스크 관리 사이의 트레이드오프를 코드로 구현했습니다.

핵심 포인트

  • AI를 활용해 복잡한 결제 아키텍처의 프로토타입을 빠르게 구현
  • Cloudflare Workers와 D1을 사용한 서버리스 기반 참조 모델 구축
  • 결제 금액에 따른 레인 분리(Express/Standard vs High Value) 설계
  • 대납(Advancing) 리스크와 실시간 결제(RTGS) 간의 설계적 균형 제시

최근 요시다마치나 카스미가세키 쪽에서 '세계 최고 수준의 결제 아키텍처'라든가, '온체인과 오프체인의 최적 조합' 같은 기세 좋은 페이퍼들이 쏟아져 나오고 있는 것 같다. '갈라파고스화(Galapagosization)를 회피하고 2030년까지 레거시 시스템을 근본적으로 업데이트하기 위한 7가지 방향성'과 같은 주제로, '메시징과 네팅은 누가 담당할까? A안? D안?', '지휘탑은 어디에 둘 것인가?'라며 각지에서 논의가 시작되고 있는 듯하다.

방향성은 전부 알겠다. 아는데, 이런 건 만져볼 수 있는 코드가 하나만 있어도 논의의 해상도가 완전히 달라진다고 생각한다. 요구사항 정의 파워포인트가 두꺼워지기 전에, '일단 이런 형태로 될 것 같은데'라는 초안이 있으면 이야기가 빠르다.

그래서 기다리다 못해 Cloudflare Workers + D1을 사용해서 '차세대 결제 협조층(Zenith Coordinator)'의 참조 구현(Mock)을 AI에 거의 전적으로 맡겨 개인 개발로 만들어 보았다. 솔직히 말하면, 나는 '여기는 이런 원칙으로 해달라'고 방향만 던지고 리뷰했을 뿐이다.

  • GitHub: pochatt/Zenith-Coordinator (소스 전체 공개・MIT 라이선스)

개발 과정에서 '오, 이건 설계 판단으로서 흥미롭다'라고 느낀 부분을 자랑 반, 공유 반으로 적어본다. 이것은 어디까지나 개인의 모형(Mock)일 뿐 실제 운영에는 무리가 있으니 그 점을 염두에 두고 읽어주었으면 한다.

참고로 이 시스템의 이름은 Zenith Coordinator이다. AI가 지은 것이라 그런지, 이름에 비해 조금 부족할지도 모르겠다(웃음).

페이퍼들이 요구하는 '물건의 인계와 결제를 동시에 실행하는 프로그래머빌리티'. 이것을 순수하게 '1대1 은행 송금'의 연장선으로 하려고 하면 막힌다. 그렇다고 N대M 동시 결제나 해시 타임 락(HTLC) 같은 스마트 컨트랙트 기능 때문에 거대한 은행의 레거시 계정계를 처음부터 다시 구축할 것인가? 2030년 안에 맞출 수 없다.

그래서 화려한 스마트 컨트랙트 이야기는 접고, '협조층(Zenith / ZC)이 돈을 전혀 보유하지 않는다'는 전제를 세워봤다(

반대로 **예외적인 것이 고액 레인(HIGH_VALUE)**인데, 이쪽은 별단예금(Segregated Deposit)도 H(한도)도 건너뛴다. 처음에는 "고액 결제일수록 a→b를 빨리 확정시키고 싶기 때문이겠지"라고 생각할 수도 있지만, 이 시스템에서는 그 반대를 택했다. HIGH_VALUE의 a→b는 다른 레인보다 오히려 느리다——코드 내의 전이(transition) 주석에 적힌 대로 (a_HV) → waiting for IGS → b이다.

즉, b를 내보내기 전에 중앙은행 측의 결제 완료를 기다리는 단계가 한 단계 삽입된다.

EXPRESS/STANDARD는 자금을 별단예금에 둔 채 그 자리에서 바로 b를 내보내고, 은행들 사이에서 실제로 돈을 움직이는 작업(DNS, 다음 날 아침 EOD 배치로 일괄 처리하는 넷 세틀먼트(Net Settlement))은 뒤로 미룬다. b를 내보낸 시점에는 아직 은행 간 결제 자체가 끝나지 않았으므로, 수취 측 은행은 "상대 은행이 나중에 제대로 지급할 것이다"라는 전제하에 한발 앞서 확정을 내리는, 즉 대납(立て替え, advancing)하고 있는 상태가 된다. 이 대납의 상한선이 H이며, 코드 주석에도 "held until DNS settlement" (DNS(일일 넷 세틀먼트)의 리스크 관리 프레임)라고 명시했다.

고액 결제는 이 대납 프레임에 얹기에는 금액이 너무 크다. 그래서 HIGH_VALUE는 순서를 뒤집어, 먼저 은행 간의 실제 결제(중앙은행을 매개로 한 RTGS/IGS)를 완료시키고, 이를 확인한 뒤에 b를 내보낸다(bankExecuteDebit의 HV 분기는 고객 계좌에서 청산 전용 중계 계정(ZCS)으로 직접 이체할 뿐, 별단예금은 경유하지 않는다). b를 빨리 내보내는 대신 대납할 것인가, 대납을 제로로 만드는 대신 b를 늦출 것인가——고액 결제는 후자를 선택했다는 이야기다.

포인트는, "블록체인에 자금을 싣는 것"도 "계정계를 새로 만드는 것"도 아니라, ZC는 상태와 프레임만 보유하고 자금의 격리는 각 은행의 별단예금에 맡겼다는 점이다. 프로그래머빌리티(Programmability)의 정체가 "별단예금으로 락(Lock) → 협조층이 일제히 확정 시그널을 보냄"으로 환원되기 때문에, 계정계의 개수를 최소화할 수 있다. ...라는 설계를 의도했다.

높으신 분들은 "기존 인프라와 신기술을 어떻게 연결할 것인가"를 논의하곤 하지만, 시스템적으로 "사상이 다른 것을 브릿지로 억지로 연결하는 것"이 가장 버그가 생기기 쉽다.

그래서 심플하게, 하나의 상태 머신(State Machine) 위에 기존 송금(TradFi)도 스마트 컨트랙트(DeFi)도 대등한 "레인(Lane)"으로서 나란히 배치해 보았다. 다리를 놓는 것이 아니라, 처음부터 같은 테이블에 앉히는 것이다.

핵심은 src/zc/orchestrator/state_machine.ts이다. 허가된 전이(transition)만을 하드코딩하여, 전부 isValidTransition을 통과하게 한다.

// src/zc/orchestrator/state_machine.ts
export const ALLOWED_TRANSITIONS: Record<TxState, TxState[]> = {
RECEIVED: ["PRECHECKED", "HTLC_LOCKED", "DECIDED_CANCEL"],
...

HTLC의 프로그래머블한 결제도, 일반적인 송금과 같은 네팅(Netting)도 전부 이 동일한 함수를 통과한다. "갈라파고스화를 방지하는 상호 운용성"이란, 끝까지 파고들면 **통일된 상태 머신을 하나 만드는** 이야기이기도 하다.

금융 인프라에서 가장 무서운 것은 "돈이 공중에 떠 있고, 로그를 봐도 이유를 알 수 없는" 상태라고 생각한다.

Zenith에서는 상태의 `UPDATE`와 감사 로그(FinalityLog)에 대한 `INSERT`를 **하나의 D1 배치로 아토믹(Atomic)하게** 실행한다 (공통 헬퍼 `transitionWithLog`, 정의는 `src/zc/lanes/_helpers.ts`. 각 레인은 이를 호출하기만 하면 된다). 또한 FinalityLog는 `prev_hash`의 해시 체인으로 구성되어, 일일 cron 작업이 전체 체인을 감사하여 단절을 감지한다.

덕분에 **"상태만 진행되고 감사 로그가 남지 않는 틈새"가 구조적으로 존재하지 않는다**. 트러블 발생 시 누가 조회하더라도 "동일한 거래 번호로 동일한 사유 코드"가 반환된다. 실제로 AI에게 코딩을 시켜보며, SQLite(D1) 상당의 구성에서도 설계에 따라 어느 정도의 감사성을 담보할 수 있다는 것은 개인적인 발견이었다.

결제 시스템을 만들 때, 기술보다 먼저 맞닥뜨리는 질문이 있다. **"돈이 돌아오지 않게 되는 경계선은 어디인가"**이다. 이를 모호하게 둔 채 구현하면 취소·실패·오송금 구제 처리가 레인마다 조금씩 다른 규칙이 되어, 나중에 "이 케이스는 돌려받을 수 있는가?"라는 질문에 아무도 즉답할 수 없게 된다.

Zenith에서는 확정점을 두 단계로 나누고 있다. **a (PAYER_EXEC_CONFIRMED) = 송금 측의 출금 확정**,

**b (PAYEE_EXEC_CONFIRMED) = 수취 측의 입금 확정 (변제 완료)**

a는 아직 중간점(취소할 여지가 남음)이며, 불가역 경계(Irreversible boundary)는 원칙적으로 b라는 한 점에 고정했다. 설계 원칙으로서 README에도 명시해 두었다.

b 이후의 구제는 Reversal (별도 거래)로 취급한다.

이를 단순한 슬로건으로 남기지 않고, 상태 머신 (State Machine) 레벨에서 구조적으로 강제했다. `ALLOWED_TRANSITIONS`를 보면 이해하기 쉽다.

PAYEE_EXEC_CONFIRMED: ["SETTLED"],


b에 도달한 거래가 전이될 수 있는 곳은 `SETTLED` (종단)뿐이며, `DECIDED_CANCEL` 등 취소 계열의 상태로는 **애초에 돌아오는 경로가 정의되어 있지 않다.** "b 이후에 롤백(Rollback)한다"는 코드 패스는, 사상으로서 금지하는 것이 아니라 상태 머신 위에 존재하지 않는 것이다.

b를 지난 후에 오류가 발견되면 어떻게 할까? **취소가 아니라, 새로운 별도 거래 (Reversal)를 기표하여 역방향 결제로 상쇄한다.** 원래 거래의 레코드는 `SETTLED` 상태 그대로 일절 변경하지 않는다. 구현은 `src/zc/cases/reversal.ts`에 분리해 두었으며, 서두의 주석에 이 원칙을 그대로 적어 놓았다.

  • Implements the spec's Reversal requirement:
  • "Cancellation after b (receiving side complete) is prohibited.
  • Remedy is performed via Reversal (a separate transaction)."

왜 이렇게까지 한 점을 결정짓는 것에 집착했느냐 하면, b가 흔들리면 다른 모든 메커니즘의 토대가 흔들리기 때문이다. FinalityLog의 "동일한 거래 번호로 동일한 이유"도, DNS_HOLD 시의 유동성 카스케이드(Liquidity cascade)도, GTID의 "모든 leg가 b와 일치할 때까지 확정되지 않는다"는 all-or-nothing 방식도, **전부 "b가 어디인가"가 하나로 정해져 있음을 전제로 하고 있다.** 역으로 말하면, 여기만 한 점으로 고정할 수 있다면 레인이 8종류가 있더라도 "결국 이 돈, 돌려받을 수 있습니까?"라는 질문에 대한 답은 항상 같은 지점(b를 지났는지 여부)만 보면 즉답할 수 있다. 수수하지만, 여기가 가장 흔들려서는 안 되는 설계 판단이었다고 생각한다.

이 부분은 직접 만들어보며 가장 뼈저리게 느낀 지점이다. 페이퍼(Paper)가 요구하는 결제 인프라는 시스템 설계뿐만 아니라 **"위기 시의 운영 규칙"**을 언어화하지 않으면 현실로 내려오지 않는다. "A안인가 D안인가"를 논하기 전에 해야 할 일이 있다.

그래서 `specs/zenith_policy.md`라는 규정 문서도 코드와 나란히 작성하게 하여, AI가 길을 잃지 않도록 했다. 원칙 중 하나는 **"No Heroics (사람이 무리해서 운영하는 설계를 금지한다)"**이다.

유동성이 고갈되었을 때 (DNS_HOLD), 운영 담당자가 밤을 새우며 DB를 직접 두드려 커버하는 설계는 악(惡)이라는 입장이다. Zenith에서는 미결·불일치는 모두 `CASE`라는 예외 관리 상태로 시스템적으로 수렴시킨다. 예외 처리를 운영 절차서로 떠넘기지 않고, **"제도화된 상태 전이"**로서 코드에 심는다. 기술 사양만 다루다 보면 이런 부분이 빠지기 쉽다는 것을 깨달았다.

...라고 페이퍼에 대한 엔지니어 나름의 답변을 나열해 왔지만, 개인의 모크(Mock)로 할 수 있는 일의 한계도 명확히 보인다.

- 본방 운영은 일절 상정하지 않았다. 은행↔협조층은 HMAC-SHA256 서명만 사용하며, **TLS/mTLS · 인증/인가 · 저장 시 암호화 · 규제 적합성은 범위 외**이다.
- 성능치는 개발 환경의 관측 결과이며, 수천만 계좌를 처리할 수 있다는 증명은 되지 않는다.
- 사양에는 적었지만 미구현된 규범 요건(토큰화 예금의 은행 간 이전, 24/365 DNS 다회 실시, 양자 내성, 일반 N:M GTID의 다통화 대응 등)이 아직 평범하게 남아 있다.

즉, **제언의 핵심(양자 내성 · 24/365 · 국제 연결 · 운영 주체)은 이 모크에서도 미달**이다. 그 점은 솔직하게 인정한다.

그러므로 이것은 "정답"이 아니라 **"초안(Draft)"**이다. 실제 금융 인프라를 만드는 사람이 본다면 "이 설계는 허술하다", "이러면 데드락(Deadlock)이 걸린다"라고 즉시 간파할 수 있을 것이며, 그래도 좋다. 오히려 그런 지적을 받고 싶어서 공개하는 것이다.

파워포인트(PowerPoint) 슬라이드가 두꺼워지기 전에, 직접 만질 수 있는 코드로 맞붙어 논쟁하는 것이 토론 속도가 더 빠릅니다. 소스 코드는 전부 GitHub에 올려두었으니, 무엇이든 참고용으로 활용해 주십시오. 2030년의 '다음'을 누군가가 진짜로 만들어 준다면 더할 나위 없이 좋겠습니다.

Happy Coding.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0