
처음에는 자산, 청약, 주문, 체결, 정산처럼 필요한 기능을 나열하면 전체 구조가 자연스럽게 잡힐 것이라고 생각했다. 하지만 실제로 설계를 시작해보니, 기능 목록만으로는 시스템의 경계를 분명하게 설명하기 어려웠다. 오히려 더 중요한 것은 각 도메인이 어떤 상태를 가지고, 어떤 이벤트를 통해 다음 상태로 이동하는지를 먼저 정의하는 일이었다.
금융 시스템은 특히 이 부분이 중요하다. 같은 데이터라도 현재 어떤 상태에 있느냐에 따라 허용되는 행위가 달라지기 때문이다. 아직 심사 중인 자산은 발행될 수 없고, 청약이 열리지 않은 상품에는 신청할 수 없으며, 검증되지 않은 주문은 체결 대상이 될 수 없다. 체결된 거래 역시 바로 끝나는 것이 아니라 정산 상태를 거쳐야 실제 보유수량과 현금 잔액에 반영된다. 결국 이 시스템은 기능의 집합이 아니라 상태의 연속적인 변화로 이해해야 한다.
이번 글에서는 조각투자증권 시스템의 핵심 도메인인 Asset, Offering, Subscription, Order, Trade, Settlement, PayoutEvent를 중심으로 상태 전이를 정리해보려고 한다. 이 글의 목적은 단순한 상태 목록 정리가 아니라, 이후 ERD와 API, 서비스 경계를 설계하기 위한 기준을 세우는 데 있다.
왜 상태 전이를 먼저 정리해야 할까
ERD부터 그리기 시작하면 보통 테이블 간 관계는 어느 정도 만들 수 있다. 하지만 그 관계만으로는 “언제 생성되는지”, “어떤 조건에서 변경되는지”, “어떤 상태일 때만 다음 작업이 가능한지”를 설명하기 어렵다. 예를 들어 Offering과 Subscription이 1:N 관계라는 사실은 쉽게 표현할 수 있지만, Offering이 OPEN 상태일 때만 Subscription을 생성할 수 있다는 제약은 관계선만으로는 드러나지 않는다.
그래서 상태 전이는 단순 부가정보가 아니라 설계의 기준이 된다.
상태 전이를 먼저 정리하면 다음과 같은 장점이 있다.
첫째, 도메인 책임이 선명해진다.
예를 들어 주문(Order)과 체결(Trade)이 별개라는 점이 명확해진다. 주문은 NEW, VALIDATED, PARTIALLY_FILLED, FILLED, CANCELED, REJECTED 같은 상태를 가지지만, 체결은 주문과 다른 생명주기를 가진다. 이걸 구분하지 않으면 OMS와 체결 엔진의 경계도 흐려진다.
둘째, 업무 규칙을 코드보다 먼저 정리할 수 있다.
청약은 OPEN 상태에서만 허용되고, 배정은 CLOSED 이후에만 가능하며, 수익분배는 기준일 스냅샷이 생성된 뒤에야 계산할 수 있다. 이런 규칙은 구현 이후에 보정하는 것이 아니라 설계 단계에서 먼저 고정해야 한다.
셋째, ERD와 API가 자연스럽게 따라온다.
상태가 정리되면 각 테이블에 어떤 상태 컬럼이 필요한지, 어떤 이벤트가 서비스 메서드로 드러나야 하는지, 어떤 상태 변경 이력이 중요한지까지 연결해서 볼 수 있다.
1. Asset 상태 전이 — 자산은 등록 즉시 거래 대상이 되지 않는다
조각투자 시스템은 투자자가 주문을 넣는 시점부터 시작되는 것이 아니라, 거래 대상이 될 자산을 등록하고 심사하는 단계에서부터 출발한다. 따라서 Asset은 단순한 정보 테이블이 아니라, “투자 가능한 자산으로 진입하는 과정” 자체를 표현하는 도메인이다.
내가 정의한 Asset의 상태는 다음과 같다.
- DRAFT
- UNDER_REVIEW
- APPROVED
- REJECTED
- LISTED
초기 상태는 DRAFT다. 자산 등록을 시작했지만 아직 제출 전인 상태다. 이 단계에서는 자산명, 설명, 평가금액, 예상수익, 증빙 문서 등이 입력될 수 있지만, 외부 투자자에게 보이는 단계는 아니다. 이후 내부 심사를 요청하면 UNDER_REVIEW 상태로 이동한다. 심사 단계에서는 자산의 구조, 소유 관계, 수익의 실현 가능성, 관련 계약의 적절성 등을 검토한다.
심사를 통과하면 APPROVED 상태가 된다. 이 상태는 “투자 상품으로 만들 수 있는 자산”이라는 의미를 가진다. 다만 이 시점에도 아직 거래 가능 상태는 아니다. 자산이 승인되었다고 해서 곧바로 투자 대상이 되는 것은 아니고, 그 다음 단계인 증권 발행과 청약 과정을 거쳐야 한다. 최종적으로 발행과 상장이 완료되면 LISTED 상태가 된다. 반대로 심사에서 반려되면 REJECTED 상태로 이동한다.
이렇게 자산 상태를 따로 두는 이유는 명확하다. 자산 자체의 심사 상태와 발행 상품의 상태를 같은 테이블에서 관리하면, “자산은 승인되었지만 아직 발행 전” 같은 상황을 표현하기 어려워진다. 따라서 자산은 자산대로 생명주기를 가져야 한다.
2. Offering 상태 전이 — 발행과 청약은 별도의 생명주기를 가진다
자산이 심사를 통과하면 그 자산을 기반으로 증권이 발행될 수 있다. 여기서 중요한 점은 Security와 Offering을 구분해서 보는 것이다. Security는 투자 가능한 증권 단위 자체를 의미하고, Offering 상태는 그 증권에 대해 실제로 청약을 진행하는 발행 라운드에 가깝다.
Offering의 상태는 다음과 같이 정의했다.
- PREPARING
- OPEN
- CLOSED
- ALLOCATION_IN_PROGRESS
- ALLOCATED
- CANCELED
발행 정보가 준비되었지만 아직 청약을 시작하지 않은 상태가 PREPARING이다. 청약 시작 시점이 되면 OPEN으로 바뀌고, 이 상태에서만 투자자의 청약 신청이 가능하다. 청약 기간이 끝나면 CLOSED가 되고, 이후에는 신청을 더 받을 수 없다.
여기서 중요한 것은 CLOSED 직후 바로 ALLOCATED로 가지 않는다는 점이다. 나는 중간에 ALLOCATION_IN_PROGRESS 상태를 두었다. 이유는 배정이 단순히 컬럼 몇 개를 업데이트하는 과정이 아니라, 경쟁률 계산, 배정 수량 산정, 환불 금액 계산, 예수금 반환, 보유수량 생성 같은 작업을 포함하기 때문이다. 이 단계는 배치 처리나 비동기 작업으로 분리될 가능성이 높기 때문에, 상태로 명확히 드러내는 편이 운영과 추적 측면에서 훨씬 낫다.
모든 배정과 환불 처리가 끝나면 ALLOCATED 상태가 된다. 발행이 취소되는 경우에는 PREPARING 또는 OPEN 상태에서 CANCELED상태로 갈 수 있다.
이 상태 전이를 정의하면서 분명해진 점은, 청약은 단순한 신청 기능이 아니라 발행시장의 핵심 프로세스라는 사실이었다. 그리고 이 생명주기를 갖는 것은 자산도 아니고, 증권도 아니며, Offering 상태이다.
3. Subscription 상태 전이 — 투자자의 청약 신청도 독립된 상태를 가져야 한다
발행이 열리면 투자자는 청약을 신청할 수 있다. 이때 Subscription은 단순한 join 테이블이 아니라, 투자자의 1차 시장 참여 이력을 표현하는 핵심 엔티티가 된다.
내가 정의한 Subscription 상태는 다음과 같다.
- REQUESTED
- CONFIRMED
- CANCELED
- ALLOCATED
- PARTIALLY_ALLOCATED
- REFUNDED
투자자가 청약 의사를 제출하면 먼저 REQUESTED 상태가 된다. 이 시점에는 아직 검증이 끝나지 않았기 때문에, 예수금 홀드나 투자 자격 검증이 이어져야 한다. 회원 상태가 정상인지, KYC가 완료되었는지, 투자 한도를 넘지 않는지, 필요한 예수금이 있는지 확인한 뒤에 CONFIRMED가 된다.
이후 투자자가 스스로 취소하면 CANCELED로 전환된다. 청약 마감 후 배정 결과가 나오면 전량 배정은 ALLOCATED, 일부 배정은 PARTIALLY_ALLOCATED로 본다. 그리고 미배정 금액 환불까지 완료되면 REFUNDED 상태를 가질 수 있다.
처음에는 REFUNDED를 별도 상태로 둘지 고민이 있었다. 단순히 refund_amount 같은 컬럼만 두고 끝낼 수도 있었기 때문이다. 하지만 금융 시스템에서는 “환불 예정”과 “환불 완료”의 차이가 중요하다고 판단했다. 따라서 상태로 명확히 구분하는 편이 좋다고 생각한다. 이런 결정은 나중에 정산과 원장 반영을 설계할 때도 일관성을 줄 것이다.
4. Order 상태 전이 — 주문은 체결과 분리되어야 한다
유통시장이 열리면 투자자는 증권을 사고팔 수 있다. 여기서 가장 중요한 설계 포인트 중 하나는 Order와 Trade를 반드시 분리해서 보는 것이다. 주문은 거래 의사이고, 체결은 그 의사가 실제로 성사된 결과다. 이 둘은 같은 생명주기를 가질 수 없다.
Order 상태는 다음과 같다.
- NEW
- VALIDATED
- PARTIALLY_FILLED
- FILLED
- CANCELED
- REJECTED
주문이 접수되면 NEW다. 이때는 아직 시스템이 이 주문을 체결 대상으로 올릴지 판단하지 않은 상태다. 이후 OMS에서 회원 상태, KYC, 거래 가능 종목 여부, 예수금 또는 보유수량 충족 여부를 확인하고 나면 VALIDATED가 된다. 이 상태가 되어야만 체결 엔진이 이 주문을 다룰 수 있다.
체결이 일부 수량만 일어나면 PARTIALLY_FILLED, 전량 체결되면 FILLED, 체결 전 사용자가 취소하면 CANCELED, 검증 실패 시에는 REJECTED 상태가 된다.
이 중 특히 VALIDATED 상태를 따로 둔 이유는 주문 검증 책임을 체결 엔진에 떠넘기지 않기 위해서다. OMS는 주문을 “체결 가능한 상태”로 만드는 역할을 담당하고, 체결 엔진은 검증이 끝난 주문끼리 가격·시간 우선 규칙에 따라 매칭하는 역할에 집중해야 한다. 상태를 분리하면 이 책임이 훨씬 선명해진다.
5. Trade 상태 전이 — 체결은 또 다른 생명주기의 시작이다
매수 주문과 매도 주문이 조건에 맞게 만나면 Trade가 생성된다. 많은 시스템에서 체결은 거래의 끝처럼 보이지만, 실제로는 정산의 시작점이다. 따라서 Trade 역시 독립된 상태를 가져야 한다.
정의한 상태는 다음과 같다.
- MATCHED
- SETTLEMENT_PENDING
- SETTLED
- SETTLEMENT_FAILED
체결 엔진이 거래를 생성하는 순간 MATCHED가 된다. 이후 정산 프로세스로 넘겨지면 SETTLEMENT_PENDING, 실제 현금과 보유수량 이동이 모두 완료되면 SETTLED, 정산 중 오류가 발생하면 SETTLEMENT_FAILED 상태가 된다.
이렇게 Trade에 정산 연계 상태를 두는 이유는 운영 추적 때문이다. 나중에 거래 내역을 조회할 때 단순히 “체결되었는가”만 보는 것이 아니라, “정산까지 끝났는가”를 같이 봐야 하기 때문이다. 특히 금융 시스템에서는 거래 레코드와 정산 record가 논리적으로 이어져야 사고 대응이 쉬워진다.
6. Settlement 상태 전이 — 금융 시스템은 결국 정산을 어떻게 다루느냐의 문제다
체결 이후 실제 반영을 담당하는 것이 Settlement다. 이 도메인은 단순 후처리가 아니라, 매수자와 매도자의 현금과 보유수량을 원자적으로 바꾸는 핵심 계층이다.
정의한 상태는 다음과 같다.
- PENDING
- PROCESSING
- COMPLETED
- FAILED
- REVERSED
체결 직후 정산 건이 생성되면 PENDING, 실제 처리 시작 시 PROCESSING, 모든 원장 반영이 끝나면 COMPLETED, 오류 발생 시 FAILED다. 그리고 이후 사후 정정이나 취소 처리가 필요할 경우 REVERSED 상태까지 고려했다.
여기서 REVERSED를 넣은 것은 금융 시스템의 특성을 반영해보려고 했다. 단순한 서비스라면 잘못된 정산을 삭제하고 다시 처리하는 방식도 가능하겠지만, 금융 시스템에서는 이미 발생한 이벤트를 그냥 지워버리는 방식이 매우 위험하다고 KB IT's your life에서 배웠다. 오히려 정정 이력을 별도의 이벤트로 남기는 것이 맞다. 이 상태는 실제 MVP 구현에서는 후순위일 수 있다. 하지만 설계 단계에서는 미리 고려하는 편이 옳다고 생각한다.
7. PayoutEvent 상태 전이 — 조각투자의 차별점은 거래 이후에도 계속된다
조각투자 시스템을 일반 거래 플랫폼과 구분하는 중요한 요소 중 하나는 수익분배다. 조각투자는 많은 경우 기초자산에서 발생한 수익을 투자자에게 나누어주는 구조를 가지기 때문에, 거래 이후에도 생명주기가 이어진다.
PayoutEvent의 상태는 다음과 같다.
- SCHEDULED
- SNAPSHOT_CREATED
- CALCULATED
- PAID
- FAILED
- CANCELED
분배 이벤트가 생성되면 SCHEDULED, 기준일 시점의 보유수량을 스냅샷으로 생성하면 SNAPSHOT_CREATED, 투자자별 지급 금액 계산이 끝나면 CALCULATED, 실제 예수금 반영까지 완료되면 PAID, 오류가 발생하면 FAILED, 이벤트가 철회되면 CANCELED로 간다.
특히 SNAPSHOT_CREATED를 별도 상태로 둔 것은 중요하다. 수익분배는 현재 보유수량이 아니라 기준일 시점의 보유수량을 기준으로 이루어져야 하기 때문이다. 이 개념이 없으면 분배의 정당성과 재현 가능성이 사라진다. 또한 계산 완료와 지급 완료를 나누면, 배치 처리와 운영 모니터링도 훨씬 명확해진다.
상태 전이를 연결해보면 전체 흐름이 보인다
각 상태를 따로 보면 단순한 enum 집합처럼 보일 수 있다. 하지만 이를 연결해서 보면 전체 시스템의 뼈대가 드러난다.
자산은 DRAFT에서 시작해 심사를 거쳐 APPROVED가 되고, 그 자산을 기반으로 Offering이 생성된다. Offering이 OPEN 상태가 되면 투자자는 Subscription을 만들 수 있다. 청약이 마감되고 ALLOCATED가 되면 투자자는 보유수량을 갖게 되고, 그 이후에야 Order를 생성할 수 있다. 검증이 끝난 주문은 체결 엔진에서 매칭되어 Trade가 생성되고, 이 거래는 다시 Settlement를 거쳐 실제로 보유수량과 예수금에 반영된다. 이후 자산에서 수익이 발생하면 PayoutEvent가 생성되고, 기준일 스냅샷과 계산을 거쳐 최종 지급까지 이어진다.
즉, 조각투자증권 시스템의 핵심은 기능 자체보다 언제 어떤 상태가 허용되고, 어떤 상태가 다음 도메인을 열어주는가를 정의하는 데 있다.
이 정리가 이후 설계에서 어떤 의미를 가지는가
상태 전이를 정리하고 나니 이후 설계 방향도 훨씬 분명해졌다. 먼저 ERD에서는 각 테이블이 어떤 상태 컬럼을 가져야 하는지가 뚜렸해졌다. 또한 어떤 엔티티를 분리해야 하는지도 더 분명해졌다. 예를 들어 Order, Trade, Settlement를 각각 분리된 테이블로 가져가야 하는 이유가 상태 모델만으로도 충분히 설명된다.
서비스 경계도 자연스럽게 잡힌다. Offering의 상태를 변경하는 책임은 발행/청약 도메인에 있고, Order를 VALIDATED로 바꾸는 책임은 OMS에 있으며, Trade를 생성하는 책임은 체결 엔진에 있다. Settlement와 PayoutEvent는 다시 원장과 정산, 수익분배 도메인으로 이어진다. 상태를 먼저 정의했기 때문에 시스템을 서브시스템 단위로 나눌 때도 억지스럽지 않다.
결국 이번 상태 전이 정리는 단순한 사전 작업이 아니라, 이 프로젝트 전체를 관통하는 기준을 세우는 과정이었다. 조각투자증권 시스템은 결국 “데이터를 저장하는 시스템”이 아니라, 정해진 상태와 규칙에 따라 안전하게 상태를 바꾸는 시스템에 더 가깝다. 그리고 이 관점을 분명히 한 뒤에야, 비로소 ERD와 원장 구조를 제대로 그릴 수 있게 된다.
다음 글에서는 이 상태 전이를 바탕으로, 전체 ERD를 어떻게 나눴고 각 엔티티를 어떤 관계로 연결했는지 정리해보려고 한다. 특히 자산과 증권, 발행과 청약, 주문과 체결, 체결과 정산, 원장과 집계 테이블을 왜 분리했는지를 중심으로 살펴볼 예정이다.
'조각투자증권 시스템 설계' 카테고리의 다른 글
| 조각투자증권(STO) 거래 시스템의 전체 LifeCycle (0) | 2026.03.23 |
|---|---|
| 조각투자증권(STO) 거래 시스템 설계 프로젝트를 시작하며 (0) | 2026.03.22 |