토스급 핀테크 시스템 설계: 계좌 연결·송금·결제·원장·인증 오케스트레이션
사용자가 계좌를 연결할 때 허용한 범위와 만료 시점은 결제가 끝날 때까지 영향을 준다. 토스급 핀테크 시스템 설계에서 먼저 붙잡을 객체는 서버 목록이 아니라 account_link다. 이 객체가 유효해야 연결 계좌로 결제를 시작할 수 있고, 이후 앱 응답·외부 조회·웹훅·정산 명세가 엇갈려도 한 결과로 모을 수 있다.
이 글은 2026년 8월 15일 기준 금융결제원, IETF, OpenID Foundation, EMVCo 등 공개 자료를 대조한 공급자 중립 참조 설계다. 실제 토스의 비공개 구조나 처리량을 설명하지 않는다. 모바일·웹 사용자 한 명이 계좌 접근에 동의하고 가맹점 결제를 승인한 뒤 운영자가 차이를 조정하는 여정을 따라간다. 규모·지연·보존 기간은 운영 실측값이 아니라 선택을 비교하기 위한 설계 가정이다.
20초 핵심 요약
- 무엇: 계좌 접근 동의로 시작한 결제를 외부 결과, 자체 원장, 사용자 화면과 가맹점 주문에 일관되게 반영하는 참조 설계다.
- 왜: 타임아웃을 실패로 오판하거나 늦은 웹훅을 그대로 덮어쓰면 중복 출금, 잘못된 잔액과 결제됐는데 이행되지 않은 주문이 생길 수 있다.
- 어떻게: 동의 수명주기, 멱등 키, 허용된 상태 전이, 복식 원장과 조정을 연결해 서로 다른 확정 수준의 증거를 한 결과로 수렴시킨다.
한 결제는 계좌 동의에서 시작해 조정에서 끝난다
주 사용자는 여러 금융기관의 계좌와 카드를 한 앱에 연결해 송금하거나 온라인 결제를 하는 개인이다. 보조 사용자는 결제 결과로 주문을 처리하는 가맹점과 내부·외부 기록의 차이를 조사하는 운영자다. 앱과 웹은 화면과 기기 인증을 맡지만 돈의 최종 상태를 스스로 정하지 않는다.
이 글의 대표 여정은 세 행동으로 고정한다.
- 사용자는 연결할 계좌와 조회·이체 범위를 고른다. 허용한 계좌만 보여야 하며 철회·만료 시에는 재인증이 필요하다는 결과를 받아야 한다.
- 사용자는 연결 계좌로 가맹점 결제를 승인한다. 한 번 누른 결제는 업무상 한 번만 반영되고, 추가 인증이 필요하면 돈을 보내기 전에 그 이유가 보여야 한다.
- 가맹점은 최종 결제 결과로 주문을 이행하고 운영자는 불일치를 조정한다. 결과가 늦으면 성공·실패를 추측하지 않고 무엇을 확인 중인지 알려야 한다.
포함 범위는 계좌 연결 동의, 조회, 송금, 결제 승인·확정·취소·환불, 인증, 원장, 웹훅, 조정과 장애 격리다. 대출 심사, 투자 주문, 환율 엔진, 카드 네트워크 자체 구축과 사기 탐지 모델의 구체 알고리즘은 제외한다. 개인정보는 사용자가 허용한 범위로 제한하고 계좌 식별자, 토큰과 인증 자료를 화면·로그·분석 시스템에 불필요하게 남기지 않는다.
조회는 마지막 갱신 시각과 함께 빠르게 보여준다. 결제·송금 접수는 수 초 안에 확정 결과나 의미가 분명한 처리 중 상태를 돌려준다. 외부 기관 응답이 늦으면 결과를 만들어내지 않는 것이 정확성 요구사항이다.
규모 가정은 결제보다 조회와 증거 저장이 먼저 커질 수 있음을 보인다
아래 값은 실제 토스 수치가 아니다. 입력, 계산 결과와 설계 영향을 함께 둔 가상 시나리오다.
| 항목 | 가정 입력 | 계산 결과 | 설계에 미치는 영향 |
|---|---|---|---|
| 월간 활성 사용자 | 2,000만 명 | 가정 | 사용자 ID와 계좌 연결을 나눌 기준이 필요하다 |
| 일간 활성 비율 | 40% | 800만 명 | 조회·인증 요청이 금액 쓰기보다 많다 |
| 사용자당 하루 홈 조회 | 8회 | 6,400만 회/일, 평균 약 741 RPS | 매 화면에서 모든 기관을 동기로 호출하지 않는다 |
| 사용자당 하루 송금 | 0.15회 | 120만 건/일, 평균 약 14 TPS | 처리량보다 중복 방지와 순서를 우선한다 |
| 사용자당 하루 결제 | 0.5회 | 400만 건/일, 평균 약 46 TPS | 거래마다 여러 증거와 상태 이벤트가 생긴다 |
| 연결 계좌 | 평균 4개 | 8,000만 연결 | 동의 만료 인덱스와 기관별 갱신이 필요하다 |
| 원장 행 | 금액 거래당 평균 4행 | 2,080만 행/일, 연 약 75.9억 행 | 시간·계정별 분할과 요약 잔액을 검토한다 |
| 이벤트 | 금액 거래당 평균 8개 | 4,160만 건/일 | 상태 이벤트를 원장 쓰기와 분리하되 유실을 막는다 |
홈 조회 계산은 8,000,000 × 8 ÷ 86,400 ≈ 741이다. RPS(Requests Per Second)는 1초에 들어오는 요청 수이고, TPS(Transactions Per Second)는 1초에 처리하는 금액 거래 수다. 피크를 조회 10배, 송금·결제 20배로 가정하면 각각 약 7,410 RPS, 278 TPS, 926 TPS다.
fan-out은 한 요청이 여러 하위 요청으로 퍼지는 현상이다. 홈 화면이 연결 계좌 4개를 동시에 외부 조회하면 호출량과 장애 노출이 네 배가 되므로, 마지막 갱신 시각이 있는 잔액과 응답 뒤 갱신을 조합한다. 계좌 수나 거래당 이벤트가 두 배면 관련 저장량과 호출량도 두 배다.
숫자 하나만 보고 처음부터 분산하지 않는다. 피크 금액 쓰기가 단일 관계형 데이터베이스의 안정 처리량을 넘거나 한 기관 장애가 다른 기관의 대기열까지 늦출 때 원장 분할과 기관별 격리를 시작한다.
첫 행동: 계좌 연결은 영구 등록이 아니라 만료되는 권한이다
금융결제원 공식 설명상 사용자인증 API는 인증·동의·계좌등록을 수행한다. 등록 결과인 핀테크이용번호는 클라이언트와 사용자 쌍에 고유하며 조회·이체 호출에 쓸 수 있다. 따라서 account_link는 단순한 계좌 행이 아니라 동의 범위·만료·철회 상태를 가진 권한 객체다.
- 사용자가 ‘계좌 연결’을 누르면
POST /v1/account-link-sessions가 일회성 연결 세션과 OAuth(OAuth 2.0, 비밀번호를 앱에 넘기지 않고 제한된 접근 권한을 주는 방식)state를 만든다.state는 시작한 요청과 돌아온 응답이 같은 흐름인지 확인하기 위해 필요하다. - 인증 오케스트레이터가 은행 또는 오픈뱅킹 인증 화면으로 보낸다. 오케스트레이터는 기관별 차이를 공통 동의 범위와 만료 정보로 바꾸는 역할만 맡는다.
- 사용자는 본인 인증 뒤 조회·이체 범위를 고른다. 앱 로그인과 금융 계좌 접근 동의는 서로 다른 권한이다.
- 콜백에서
state, 발급자와 PKCE(Proof Key for Code Exchange, 요청마다 만든 검증값으로 인증 코드를 처음 요청한 쪽에 묶는 방식)를 확인한다. 탈취된 인증 코드의 재사용을 막기 위해 필요하다. - 토큰과 외부 계좌 식별자는 암호화 저장소에 두고, 일반 조회 데이터에는 내부 연결 ID, 기관, 가린 표시값, 동의 범위와 만료만 둔다.
- 계좌 연결 뒤 잔액 조회가 늦으면 연결은
ACTIVE, 잔액은STALE로 나눠 표시한다.STALE은 값은 있지만 갱신 목표보다 오래됐다는 뜻이며 사용자가 신선도를 판단하는 데 필요하다. - 동의 철회나 토큰 만료가 확인되면
REAUTH_REQUIRED또는REVOKED로 바꾸고 새 금액 명령을 막는다.
POST /v1/account-link-sessions
Authorization: Bearer {ACCESS_TOKEN}
Content-Type: application/json
{"provider":"KFTC_OPEN_BANKING","scopes":["balance.read","transactions.read","transfer.write"],"redirect_uri":"app://account-link/callback"}
{"link_session_id":"als_example_001","authorization_url":"https://provider.example/authorize/request-001","expires_at":"2026-08-15T02:00:00Z"}
둘째 행동: 연결 계좌 결제는 네 증거의 확정 수준을 비교한다
멱등성은 같은 명령을 여러 번 보내도 최초 한 번을 넘는 업무 부작용이 생기지 않는 성질이다. 가맹점이 만든 멱등 키를 결제 명령의 식별자로 저장해야 중복 탭과 타임아웃 뒤 재시도가 새 결제가 되지 않는다.
- 가맹점 체크아웃이
POST /v1/payment-intents로 주문 참조, 금액, 통화, 연결 계좌와 멱등 키를 보낸다. Payment Intent는 여러 시도를 한 결제의 수명주기로 묶는 객체이며, 취소·환불까지 같은 원거래를 추적하기 위해 필요하다. - API는 가맹점의 주문 소유권, 금액, 유효한
account_link와 동의 범위를 검사한다. 같은 호출자·키가 이미 있으면 기존 결제 ID와 저장한 결과를 돌려준다. - 위험 정책이 추가 인증을 요구하면
REQUIRES_AUTH로 두고 인증을 시작한다. 카드라면 3-D Secure(3DS, 온라인 결제에서 카드 소유자를 추가 확인하는 절차)가 이 역할을 할 수 있다. 연결 계좌 결제에는 기관이 요구하는 별도 확인을 적용한다. - 인증이 끝난 뒤에만 외부 제공자에게 승인 또는 출금 명령을 보낸다. 빠른 확정은
AUTHORIZED나FAILED, 늦게 끝나는 처리는PROCESSING, 응답을 받지 못한 호출은UNKNOWN으로 둔다. - 확정된 금액 변화만 원장에 새 행으로 기록한다. 가맹점에는 내부 구현 상태를 그대로 노출하지 않고 주문 이행에 필요한 공개 상태와 조회 API를 제공한다.
- 서명 검증 웹훅, 제공자 상태 조회와 정산 명세가 늦게 오면 모두 같은 상태 전이 API를 호출한다.
POST /v1/payment-intents
Idempotency-Key: <merchant-generated-unique-key>
Content-Type: application/json
{"merchant_id":"mer_example_001","order_ref":"order_example_001","account_link_id":"al_example_001","amount_minor":12900,"currency":"KRW","capture_method":"AUTOMATIC"}
금액은 계산 오차를 피하려고 최소 화폐 단위의 정수 amount_minor로 받는다. 같은 멱등 키에 다른 주문이나 금액이 오면 409 IDEMPOTENCY_CONFLICT로 거절한다. TTL(Time To Live)은 데이터를 자동 만료하기까지의 시간이다. 멱등 기록의 TTL이 늦은 재시도나 분쟁 기간보다 짧으면 과거 요청이 새 결제가 될 수 있으므로, 고정된 24시간을 복제하지 않고 업무 위험과 외부 참조 고유 조건에 맞춘다.
결과 신호는 단순히 마지막에 도착한 순서로 고르지 않는다.
| 증거 | 빠른 이유와 한계 | 맡는 역할 | 단독 확정 금지 조건 |
|---|---|---|---|
| 동기 API 응답 | 요청 경로에서 빠르지만 응답 직전 연결이 끊길 수 있다 | 최초 사용자 표시와 빠른 확정 | 타임아웃·전송 오류를 실패로 바꾸지 않는다 |
| 제공자 상태 조회 | 현재 상태를 다시 묻지만 호출 한도와 일시 오류가 있다 | UNKNOWN을 능동 확인 |
조회 실패를 결제 실패로 바꾸지 않는다 |
| 서명 검증 웹훅 | 빠르게 밀어주지만 중복·순서 역전·누락될 수 있다 | 비동기 상태 갱신 | 이벤트 중복 제거와 허용 전이 검사 전 원장 반영을 막는다 |
| 정산·잔액 명세 | 늦지만 기간 전체의 금액과 누락을 대조한다 | 최종 차이 탐지와 운영 조사 | 근거가 불충분한 차이는 수동 검토로 보낸다 |
다음 그림은 왼쪽의 동의 생성부터 결제 명령, 네 증거, 상태 전이와 원장, 가맹점 주문 순서로 읽으면 된다. 아래쪽 조정 화살표가 빠진 웹훅과 충돌한 응답을 어떻게 다시 같은 규칙으로 돌려보내는지도 함께 본다.

그림의 결론은 가장 늦게 온 신호가 이기는 것이 아니라, 출처·외부 참조·관측 시각·현재 상태 버전과 허용 전이를 모두 통과한 증거만 상태를 바꾼다는 점이다. 이미 정산으로 확정된 성공을 늦은 실패 웹훅이 되돌려서는 안 된다.
돈의 기록과 결제 진행 상태는 서로 다른 표에 둔다
거래 테이블은 사용자가 시작한 업무의 진행 상태를 담는다. 원장은 돈의 이동을 지우지 않고 기록하는 장부이며 잔액과 감사의 근거가 필요하기 때문에 별도로 둔다. 화면용 읽기 데이터는 둘을 빠르게 보여주지만 최종 돈 상태를 결정하지 않는다.
| 데이터 | 주요 키와 제약 | 인덱스·보존 판단 |
|---|---|---|
account_links |
연결 ID, 사용자, 기관, 암호화한 외부 참조, 동의 범위·만료 | (user_id, status), (provider, consent_expires_at)로 목록과 만료 작업을 찾는다 |
payment_intents |
결제 ID, 가맹점 주문 참조, 금액, 상태 버전, 외부 참조 | (merchant_id, order_ref)와 (provider, provider_payment_ref)를 고유하게 만든다 |
idempotency_records |
호출 범위+키, 요청 본문 해시, 생성 리소스와 응답 | 만료 인덱스를 두되 기간은 분쟁·재시도 조건으로 정한다 |
ledger_transactions·ledger_entries |
업무 유형+업무 ID, 원거래 참조, 계정별 차변·대변 | 계정과 시간으로 조회하며 과거 금액 행은 수정·삭제하지 않는다 |
inbox_events·outbox_events |
제공자+이벤트 ID, 보낼 이벤트와 발행 여부 | 받은 증거의 중복 제거와 보내지 못한 가맹점 알림 재시도에 쓴다 |
reconciliation_items |
기관, 영업일, 외부·내부 참조, 금액과 차이 유형 | 기관·영업일·해결 상태로 조사 대상을 찾는다 |
append-only는 과거 기록을 고치지 않고 새 행만 추가하는 방식이다. 원장은 잘못된 거래도 삭제하지 않고 원거래를 가리키는 반대 거래를 추가한다. 복식 원장은 모든 금액 변화를 같은 액수의 차변과 대변으로 남기는 장부 방식이다. 이 글에서는 돈의 출처와 목적지를 함께 기록해 근거 없이 생기거나 사라진 금액을 검출하기 위해 사용한다.
status_version은 늦은 증거가 최신 상태를 덮지 못하게 하는 번호다. 예를 들어 이미 SUCCEEDED 버전 7인 결제에 오래된 FAILED 증거가 와도 현재 상태와 허용 전이가 맞지 않으면 거절한다. 잔액 조회용 요약은 둘 수 있지만 처리한 원장 위치를 함께 저장해 원장 기록에서 다시 만들 수 있어야 한다.
보존 기간은 이 설계에서 확정하지 않는다. 거래·동의·접근 로그는 최신 법률과 기관별 계약에 따라 별도 정책표로 정하고, 법적 보존 대상과 사용 목적이 끝난 개인정보를 구분한다.
상태 머신은 앱·제공자·원장·주문의 네 상태를 억지로 합치지 않는다
상태 머신은 한 객체가 가질 수 있는 상태와 허용된 이동을 미리 정한 규칙이다. 결제는 REQUIRES_PAYMENT_METHOD → REQUIRES_AUTH → PROCESSING → AUTHORIZED → CAPTURED → SUCCEEDED를 기본 흐름으로 둔다. 중간에 FAILED, CANCELED, UNKNOWN으로 갈 수 있고, 성공 뒤 환불은 REFUND_PENDING → PARTIALLY_REFUNDED | REFUNDED | REFUND_FAILED로 이어진다. 상태 이름은 공급자 중립 설계 가정이다.
PROCESSING은 제공자가 아직 처리 중임을 안다는 뜻이다. UNKNOWN은 요청을 보냈지만 응답을 못 받아 실행 여부를 모른다는 뜻이다. FAILED는 명시적 거절이나 미실행 증거가 있을 때만 쓴다. 셋을 합치면 가맹점이 주문을 이행해도 되는지, 운영자가 재호출해도 되는지 판단할 수 없다.
자체 결제의 SUCCEEDED가 곧 가맹점 주문의 FULFILLED는 아니다. 가맹점 알림이 실패할 수 있기 때문이다. outbox는 결제 상태 변경과 보낼 이벤트를 같은 데이터베이스 처리에 저장해 “결제는 성공했지만 주문 알림은 사라진” 틈을 막는 방식이다. 가맹점은 웹훅뿐 아니라 결제 상태 조회 API로도 결과를 확인한다.
송금은 이 규칙의 짧은 대조 사례다. 사용자가 수취인과 금액을 확인하면 멱등 키와 함께 POST /v1/transfers를 보내고, 인증 뒤 금액을 예약한 다음 외부 이체를 호출한다. 명시적 성공은 SUCCEEDED, 거절은 FAILED, 응답 없음은 UNKNOWN이다. 금융결제원 입금이체 API도 응답을 받지 못한 경우 별도 처리가 필요하다고 명시하므로 타임아웃을 새 송금으로 재시도하지 않는다. 결제와 마찬가지로 외부 참조의 고유 제약, 원장과 조회·웹훅·조정이 결과를 확정한다.
웹훅 누락은 조정이 찾고 같은 전이 API가 복구한다
웹훅은 외부 시스템이 상태 변화를 서버로 알려주는 HTTP 요청이다. 빠르지만 중복·순서 역전·누락될 수 있으므로 원문과 헤더로 서명을 확인한 뒤 (provider, event_id)를 저장한다. 이미 처리한 ID면 성공 응답만 돌려주고, 새 이벤트는 빠르게 접수한 뒤 대기열에서 허용 상태 전이를 검사한다.
reconciliation은 조정이라는 뜻으로, 내부 원장과 외부 제공자의 거래·잔액 명세를 같은 기준일과 식별자로 맞춰 차이를 찾고 해소하는 작업이다. 웹훅이 놓친 결제나 운영 실수를 찾는 느린 안전망이 필요하기 때문에 둔다.
- 제공자별 거래·정산 파일을 읽기 전용으로 가져와 원본 해시와 기준 시각을 기록한다.
- 외부 참조, 금액, 통화와 영업일로 내부 결제와 맞춘다.
외부만 있음,내부만 있음,금액 다름,상태 다름,중복으로 나눈다.- 자동 수정 가능한 차이도 SQL로 직접 고치지 않고 같은 상태 전이 API와 원장 반대 거래로 복구한다.
- 돈이 움직였는지 불명확한 차이는
MANUAL_REVIEW로 보내고 출금 가능액을 보수적으로 제한한다. - 기관·통화별 시작 잔액, 당일 변동, 지급액과 종료 잔액을 맞춰 누락이 남았는지 확인한다.
이 순서가 감지→완화→복구→정합성 확인이다. 자동 수정은 빠르다는 이유가 아니라 충분한 외부 참조와 금액 증거가 있을 때만 허용한다.
사용자가 보는 실패가 운영 지표와 복구 순서를 정한다
| 사용자에게 보이는 현상 | 감지 | 즉시 완화 | 복구 | 정합성 확인 |
|---|---|---|---|---|
| 결제 버튼을 두 번 눌렀는데 두 건처럼 보임 | 같은 멱등 키·본문 또는 외부 참조 감지 | 기존 결제 ID 반환, 새 외부 호출 차단 | 실제 중복이면 수동 검토와 반대 거래 | 주문·기관·금액·원장 참조를 대조 |
| 성공 화면 뒤 가맹점이 주문을 처리하지 않음 | outbox 미발행, 가맹점 웹훅 실패 | 결제 상태 조회 API 제공 | 서명 웹훅 재전송, 격리 대기열 재처리 | 가맹점 수신 결과와 결제 상태 대조 |
| 결제가 오래 처리 중임 | UNKNOWN 체류 시간과 조회 횟수 |
재결제 금지 안내, 금액 예약 유지 | 웹훅→조회→정산 명세→수동 검토 | 외부 참조와 원장 예약·게시 비교 |
| 연결 계좌 잔액이 오래됨 | as_of가 갱신 목표 초과 |
마지막 확인 시각 표시 | 기관별 백그라운드 갱신 | 외부 잔액과 내부 미결 금액 조정 |
| 한 기관만 계속 실패함 | 기관별 오류율·p95 지연·회로 상태 | 해당 기관만 차단 | 낮은 비율 시험 호출 뒤 점진 복구 | 장애 기간 전체 조정 실행 |
| 환불이 늦게 보임 | REFUND_PENDING 체류 시간 |
예상 상태와 확인 시각 표시 | 조회·웹훅·정산 명세로 확정 | 원결제와 반대 원장 거래 합계 확인 |
p95는 요청 100개 중 95개가 그 시간 안에 끝나는 지연 기준이다. 평균에 가려지는 느린 사용자 경험을 보기 위해 쓴다. 함께 볼 지표는 상태별 체류 시간, UNKNOWN 비율, 웹훅 지연·중복률, 대기열 최장 나이, 원장 차변·대변 불균형과 미해결 조정 금액이다.
기관별 격리와 계정별 순서가 용량의 실제 경계다
외부 API는 기관마다 지연, 호출 한도, 점검 시간과 오류 의미가 달라 가장 불규칙한 병목이다. 기관별 연결 풀, 시간 제한, 재시도 예산과 대기열을 분리한다. 회로 차단기는 연속 실패한 외부 호출을 잠시 막아 장애 전파를 줄이는 장치다. 한 은행이 실패하면 그 기관만 지연으로 표시하고 다른 기관 결제는 계속 처리한다.
재시도는 중복 부작용이 없다고 확인된 조회나 멱등 명령에만 적용한다. 지수 백오프는 재시도 간격을 점점 늘리고 무작위 차이를 더해 복구 순간의 호출 폭주를 막는다. 대기열의 가장 오래된 메시지 나이와 처리율도 봐야 복구 뒤 오래된 작업이 새 결제를 가로막는 일을 찾을 수 있다.
핫키는 특정 키에 요청이 몰리는 현상이다. 대형 가맹점 정산 계정이나 공용 중간 계정이 한 저장 구역을 과열시킬 수 있다. 사용자 잔액은 account_id별 순서를 지키고, 공용 계정은 기관·통화·정산 주기별 하위 계정으로 나눈다. 실제 핫키를 측정하기 전의 임의 분할은 조정 비용만 늘린다.
인증과 비용은 같은 분리 원칙을 따른다
앱 세션, 계좌 접근 OAuth, 고위험 결제 추가 인증과 카드 3DS를 한 토큰으로 합치지 않는다. 발급자, 사용할 API, 수명과 철회 조건이 다르기 때문이다. RFC 9700은 공개 OAuth 클라이언트에 PKCE를 요구한다. mTLS(mutual TLS, 서버와 클라이언트가 서로 인증서를 확인하는 통신)나 DPoP(Demonstrating Proof of Possession, 요청마다 개인키 보유를 증명해 토큰 사용자를 묶는 방식) 같은 송신자 제한도 탈취 토큰의 재사용을 줄이는 후보다.
FAPI 2.0(Financial-grade API 2.0, 고가치 금융 API를 위한 OAuth 보안 프로필)은 PAR(Pushed Authorization Requests, 민감한 인증 요청을 브라우저 주소가 아니라 서버 간에 먼저 전달하는 방식)과 요청별 PKCE를 요구한다. 특정 국내 기관이 이를 쓴다는 뜻은 아니며 내부 보안 기준 후보로만 삼는다. 모든 객체 API는 로그인 사용자나 가맹점이 해당 계좌·결제에 접근할 권리가 있는지 검사한다.
주 비용은 원장·이벤트 장기 저장, 외부 API 호출, 암호화 키 사용, 조정 파일 처리, 재해 복구 복제와 24시간 운영 조사다. 모든 잔액을 실시간 외부 조회하면 신선도는 높지만 호출 비용·한도·장애 노출이 커진다. 모든 기능에 독립 데이터베이스와 대기열을 처음부터 두면 격리는 좋아져도 배포·관측·조정 비용이 늘어난다.
대안은 증거 충돌을 얼마나 설명할 수 있는지로 고른다
| 선택한 설계 | 대안 | 장점과 단점 |
|---|---|---|
| 중앙 오케스트레이터가 결제 상태와 복구 순서를 관리 | 각 서비스가 이벤트만 보고 움직이는 코레오그래피 | 한 거래의 증거와 UNKNOWN 복구를 찾기 쉽지만 코드와 변경 부담이 커진다. 코레오그래피는 알림·분석처럼 돈을 바꾸지 않는 후속 작업에 쓴다 |
짧게 확정되지 않으면 PROCESSING 또는 UNKNOWN 반환 |
외부 확정까지 연결 유지 | 긴 지연과 중복 재시도를 줄이지만 사용자와 가맹점이 상태 조회를 구현해야 한다 |
| 초기에는 관계형 데이터베이스의 복식 원장 | 처음부터 전용 원장 엔진 | 업무 상태와 원장 기록을 한 처리로 묶기 쉽다. 실제 쓰기량·핫키·감사 요구가 한계를 넘으면 전용 엔진의 강한 제약과 별도 일관성 비용을 비교한다 |
| 갱신 시각이 있는 잔액 조회본 | 화면마다 외부 기관 실시간 조회 | 홈 지연과 호출량은 낮지만 오래된 시각을 표시하고 출금 가능액을 따로 판단해야 한다 |
| 원장 쓰기는 한 활성 리전 | 활성-활성 다중 리전 쓰기 | 계정별 순서와 중복 처리가 단순하지만 지역 장애 때 전환이 필요하다. 활성-활성은 장애 내성이 높아도 동시 쓰기 충돌과 조정이 복잡하다 |
중앙 오케스트레이터는 모든 일을 하는 거대 서비스가 아니다. 돈의 상태 전이와 보상 순서를 한곳에서 집행하고, 알림·분석은 이벤트로 분리한다. 이 경계가 가맹점 문의와 운영 조사를 한 거래 ID에서 이어지게 한다.
확장은 계좌 동의와 결제 증거를 대조할 능력부터 넓힌다
- 정확한 단일 경계: 소수 서비스와 관계형 데이터베이스로 시작한다. 계좌 동의 수명주기, 결제 상태 머신, 멱등 키, 복식 원장과 outbox를 먼저 구현한다.
- 외부 장애 격리: 기관별 연결·시간 제한·재시도 예산·대기열과 회로 차단기를 분리한다. 웹훅 중복 제거와 일별 조정을 자동화한다.
- 데이터와 팀 경계 분리: 계좌 연결, 결제, 송금과 원장을 책임별로 나누되 모든 금액 변경은 원장 API를 통과시킨다. 실제 핫키를 측정해 계정·통화·기관 기준 분할을 적용한다.
- 리전 장애와 대규모 복구: RPO(Recovery Point Objective, 장애 때 잃을 수 있는 데이터 시간 범위)와 RTO(Recovery Time Objective, 서비스를 다시 열기까지의 목표 시간)를 조회와 금액 쓰기에 따로 정한다. 원장 쓰기의 계획된 단일 작성자 전환과 조정 자료 지연을 함께 시험한다.
서비스 수준 목표인 SLO(Service Level Objective)는 사용자에게 약속할 측정 목표다. 서버별 평균보다 ‘계좌 동의가 만료되기 전에 재인증을 안내했는가’, ‘결제 접수 뒤 확정 결과나 의미가 분명한 처리 중 상태를 주었는가’, ‘조정 기한을 넘긴 미확정 금액이 얼마인가’를 본다. 이 글은 실제 부하 시험과 장애 주입을 하지 않았으므로 파티션 수와 복구 목표를 운영값으로 확정하지 않는다.
이 설계를 채택하지 말아야 할 조건
선택한 설계는 계좌 연결을 만료되는 권한으로 다루고, 그 권한으로 시작한 결제의 네 증거를 허용 상태 전이와 원장으로 수렴시킨다. 빠른 웹훅이 사용자 화면을 갱신하고 느린 조정이 누락과 충돌을 찾으며, 가맹점 주문은 별도 공개 상태로 이행한다.
남은 한계는 기관별 오류 코드·한도·동의 문구·보관 의무·수수료, 실측 지연과 법률 검토다. 계좌 동의의 철회·재인증을 금액 명령과 연결할 수 없거나, 외부 명세와 자체 원장을 영업일별로 대조할 운영 능력이 없다면 연결 계좌 결제를 확대하면 안 된다. 구현 전에는 최신 계약과 법률을 확인하고, 한 기관·한 결제 수단에서 동의 만료부터 웹훅 누락과 조정 완료까지 먼저 검증하는 것이 이 설계의 채택 조건이다.
계좌 동의 수명주기나 결제 조정 운영을 더 설계하려면 관련 HuntLab 글이 실제로 연결 가능한지 확인한 뒤 이어서 읽는다.
참고 링크
- 금융결제원 오픈뱅킹 개요
- 금융결제원 사용자인증 API
- 금융결제원 입금이체 API
- IETF RFC 9700: OAuth 2.0 Security Best Current Practice
- OpenID Foundation FAPI 2.0 Security Profile
- EMVCo 3-D Secure
- AWS Builders’ Library: Making retries safe with idempotent APIs
- Stripe PaymentIntent lifecycle
- Stripe Webhooks
- Stripe Balance report
- TigerBeetle Financial Accounting
- OWASP API1:2023 Broken Object Level Authorization