개인계좌와 주문 TDD (Technical Design Document)

Background

PRD 참조. 토스 Open API의 개인 계좌·주문 기능을 연동하고, CUD 요청 시 MySQL과 동기화한다. Kotlin/Spring Boot, Hexagonal Architecture + Rich Domain Model 기반.

Overview

  • 신규 도메인 패키지 3개: account, order, holding.
  • 인증: client_credentials 그랜트로 발급한 access_token을 MySQL toss_tokens 테이블에 영속화. 만료 시 동일 엔드포인트로 자동 재발급(ADR-501 참조).
  • 계좌 식별: URL 경로가 아닌 x-tossinvest-account: {accountSeq} 헤더 사용. accountSeq는 accounts 조회 응답의 숫자 시퀀스.
  • CUD 처리 흐름: Controller → UseCase → DomainService → Toss Gateway 호출 → 성공 시 Repository.save().
  • 보유종목·거래내역 읽기 흐름: Controller → UseCase → DomainService → Toss Gateway → Repository.upsert() → 응답.

Terminology

용어정의
Account토스 증권 계좌 (계좌번호·계좌명·잔고). MySQL에 upsert 영속화
Order주식 매수·매도 주문. Toss와 MySQL을 동기 상태로 유지
Holding계좌 내 보유 종목 스냅샷. 조회 시 Toss에서 fetch 후 upsert
TradeHistory계좌 거래 내역 스냅샷. 조회 시 Toss에서 fetch 후 upsert
toss_order_idToss API가 발급하는 주문 식별자. MySQL orders 테이블 unique
accountSeq계좌 시퀀스 번호(숫자). x-tossinvest-account 헤더 값으로 사용. accountNo(문자열)와 구분
selectedaccounts 테이블의 활성 계좌 플래그. 1개만 true 허용
toss_tokensaccess_token + expires_at를 저장하는 MySQL 테이블. 앱 재시작 후에도 토큰 유지
OrderStatus(Toss)Toss API 기준: OPEN(미체결), CLOSED(체결·취소 완료). 내부 MySQL status와 별도 관리

Define Problem

AS-IS

  • 계좌·주문·보유종목·거래내역 기능 없음.
  • 토스 API에 client_credentials 인증만 구현되어 있음(시세 조회용). 개인 계좌 API는 사용자 토큰 필요.

TO-BE

  • account / order / holding 3개 도메인 패키지로 기능 분리.
  • 사용자 토큰을 환경변수(TOSS_USER_TOKEN)로 주입해 개인 계좌 API 접근.
  • CUD(주문 접수·정정·취소)는 Toss 성공 후 MySQL 동기화. 읽기(holdings·trade_history)는 Toss fetch 후 upsert.

Possible Solutions

벤치마킹 참조 제품

제품명카테고리참조 URL참조 패턴
토스증권 앱개인 투자 플랫폼계좌 선택 → 종목 조회 → 주문 흐름
키움증권 OpenAPI증권사 Open API계좌번호 기반 주문·잔고 조회 패턴

방안 비교

방안설명채택미채택 사유
사용자 토큰 OAuth Code Flow브라우저 redirect → code → token 교환. 앱이 refresh_token으로 자동 갱신수동 token 복사·주입 불필요. 브라우저 1회 승인 후 자동 운영
사용자 토큰 환경변수 직접 주입TOSS_USER_TOKEN~/.zshrc에 수동 발급·주입토큰 만료 시마다 수동 재발급 필요. 운영 불편
Toss 먼저, MySQL은 비동기Toss 호출 후 이벤트로 MySQL 업데이트단일 인스턴스 로컬 도구에 복잡도 과잉. 동기 2-step이 충분
MySQL 먼저, Toss는 eventualMySQL에 저장 후 백그라운드로 Toss 전송Toss 실패 시 MySQL에 유령 주문 잔류. Toss가 진실 원천

Detail Design

AS-IS / TO-BE 비교

항목AS-ISTO-BE
계좌 조회없음GET /api/v1/accounts — Toss fetch → upsert
활성 계좌 선택없음PUT /api/v1/accounts/{accountNumber}/select
주문 접수없음POST /api/v1/orders — Toss 선행 → MySQL 후행
주문 정정·취소없음PATCH/DELETE /api/v1/orders/{id}
보유종목 조회없음GET /api/v1/accounts/{accountNumber}/holdings
거래내역 조회없음GET /api/v1/accounts/{accountNumber}/trade-history
인증 방식client_credentials(시세 조회용)OAuth Authorization Code Flow(개인 계좌용) 추가

CUD 처리 흐름 (Toss 진실 원천)

PlaceOrderUseCase.execute(command)
  └─ OrderDomainService.placeOrder(command)
       ├─ 1. OrderGateway.placeOrder() → TossOrderId
       └─ 2. (Toss 성공 시) OrderRepository.save(Order with tossOrderId, PENDING)
  • Toss 실패 → 예외 전파, MySQL 미저장.
  • 정정/취소도 동일 패턴: Toss 성공 → MySQL status 업데이트.

Component Diagram

flowchart LR
    subgraph Presentation["Presentation"]
        AC[AccountApiController]
        OC[OrderApiController]
    end
    subgraph Application["Application"]
        AU[Account UseCases]
        OU[Order UseCases]
        HU[Holding/History UseCases]
    end
    subgraph Domain["Domain"]
        ADS[AccountDomainService]
        ODS[OrderDomainService]
        HDS[HoldingDomainService]
        AGW[AccountGateway]
        OGW[OrderGateway]
        HGW[HoldingGateway]
        AR[AccountRepository]
        OR[OrderRepository]
        HR[HoldingRepository]
        THR[TradeHistoryRepository]
    end
    subgraph Infra["Infrastructure"]
        TAG[TossAccountGateway]
        TOG[TossOrderGateway]
        THG[TossHoldingGateway]
        ARI[AccountRepositoryImpl]
        ORI[OrderRepositoryImpl]
        HRI[HoldingRepositoryImpl]
        THRI[TradeHistoryRepositoryImpl]
    end
    AC --> AU
    OC --> OU
    OC --> HU
    AU --> ADS
    OU --> ODS
    HU --> HDS
    ADS --> AGW
    ADS --> AR
    ODS --> OGW
    ODS --> OR
    HDS --> HGW
    HDS --> HR
    HDS --> THR
    TAG -.->|implements| AGW
    TOG -.->|implements| OGW
    THG -.->|implements| HGW
    ARI -.->|implements| AR
    ORI -.->|implements| OR
    HRI -.->|implements| HR
    THRI -.->|implements| THR

Sequence Diagram — 주문 접수

sequenceDiagram
    participant C as OrderApiController
    participant U as PlaceOrderUseCase
    participant D as OrderDomainService
    participant G as OrderGateway
    participant R as OrderRepository
    C->>U: execute(PlaceOrderCommand)
    U->>D: placeOrder(command)
    D->>G: placeOrder(accountNumber, symbol, type, quantity, price)
    G-->>D: tossOrderId
    D->>R: save(Order(tossOrderId, PENDING))
    R-->>D: savedOrder
    D-->>U: savedOrder
    U-->>C: PlaceOrderResponse

Sequence Diagram — 보유종목 조회

sequenceDiagram
    participant C as AccountApiController
    participant U as GetHoldingsUseCase
    participant D as HoldingDomainService
    participant G as HoldingGateway
    participant R as HoldingRepository
    C->>U: execute(accountNumber)
    U->>D: getHoldings(accountNumber)
    D->>G: fetchHoldings(accountNumber)
    G-->>D: List<HoldingData>
    D->>R: upsertAll(holdings)
    R-->>D: List<Holding>
    D-->>U: List<Holding>
    U-->>C: List<HoldingResponse>

ERD

erDiagram
    ACCOUNTS {
        varchar account_number PK
        varchar account_name
        varchar account_type
        decimal cash_balance
        tinyint selected
        datetime refreshed_at
        datetime created_at
    }
    ORDERS {
        bigint id PK
        varchar toss_order_id UK
        varchar account_number
        varchar symbol
        varchar order_type
        varchar price_type
        int quantity
        decimal price
        varchar status
        datetime ordered_at
        datetime corrected_at
        datetime cancelled_at
    }
    HOLDINGS {
        bigint id PK
        varchar account_number
        varchar symbol
        int quantity
        decimal average_price
        datetime refreshed_at
    }
    TRADE_HISTORY {
        bigint id PK
        varchar toss_transaction_id UK
        varchar account_number
        varchar symbol
        varchar trade_type
        int quantity
        decimal price
        datetime traded_at
    }
  • accounts.selected: TINYINT(1). 선택 변경 시 이전 선택 행 selected=0, 신규 selected=1 (단일 트랜잭션).
  • holdings: (account_number, symbol) 복합 UK. upsert 시 quantity·average_price·refreshed_at 갱신.
  • trade_history: toss_transaction_id UK로 멱등 upsert.
  • FK 컬럼 없음 — 애플리케이션 레벨 참조 관리.

상태 전이 (Order)

PENDING ──체결──▶ COMPLETED (터미널)
PENDING ──정정──▶ CORRECTED → (재체결 가능, Toss 내부)
PENDING ──취소──▶ CANCELLED (터미널)
CORRECTED ──취소──▶ CANCELLED (터미널)
  • 전이 규칙은 OrderStatus.canTransitTo()에 캡슐화.
  • COMPLETED·CANCELLED 상태에서는 정정·취소 불가.

Testing Plan

  • Order Entity의 상태 전이 성공·실패 케이스 — COMPLETED/CANCELLED 상태에서 정정·취소 시도 시 거부 검증 (domain BehaviorSpec)
  • Account.select() / Account.deselect() Entity 메서드의 선택 전이 및 동일 계좌 재선택 멱등성 검증 (domain BehaviorSpec)
  • OrderDomainService.placeOrder() — Toss 실패 시 MySQL 미저장 확인 (application, MockK로 Gateway 모킹)
  • OrderRepositoryImpl — TestContainers(MySQL)로 toss_order_id unique 제약·upsert 동작 검증 (infrastructure)
  • OrderApiController / AccountApiController — MockMvc로 접수·정정·취소 성공·실패, 계좌 목록·선택 전환 케이스 검증 (presentation)
  • 프레임워크: Kotest(BehaviorSpec) + MockK + TestContainers.

Release Scenario

  1. docker compose up -d (MySQL 3308, 기존 컨테이너 재활용).
  2. Flyway 마이그레이션 자동 적용 (accounts, orders, holdings, trade_history).
  3. 환경변수 TOSS_USER_TOKEN 추가 주입 후 ./gradlew bootRun.
  4. 롤백: 신규 테이블 역방향 DDL 제거. 기존 기능에 영향 없음(별도 패키지).

Observability

  • 주문 접수·정정·취소 성공/실패 로그 (계좌번호 마스킹, 종목·수량·가격 포함).
  • Toss API 호출 실패 시 에러 코드·메시지 로그.
  • holdings/trade_history upsert 건수 로그.

확인된 Toss API 패턴 (직접 호출로 검증)

엔드포인트헤더비고
POST /oauth2/tokenclient_credentials. 기존 TOSS_API_KEY/TOSS_SECRET_KEY 그대로 사용
GET /api/v1/accounts계좌 목록. accountSeq(숫자)·accountNo(문자열) 반환
GET /api/v1/holdingsx-tossinvest-account: {accountSeq}보유종목
GET /api/v1/orders?status=OPEN|CLOSEDx-tossinvest-account: {accountSeq}주문 목록
POST /api/v1/ordersx-tossinvest-account: {accountSeq}주문 접수 (경로 확인됨)
  • 계좌 식별은 URL 경로 파라미터가 아닌 x-tossinvest-account 헤더로 전달.
  • 주문 목록 status 값: OPEN(미체결) / CLOSED(완료). PENDING 아님.
  • 매수가능금액·수수료·거래내역 엔드포인트는 공식 문서에서 정확한 경로 추가 확인 필요.

주의사항

  • 토큰 MySQL 영속화: toss_tokens 테이블에 access_token과 expires_at를 저장. 앱 재시작 후에도 토큰 재사용. 만료 시 동일 POST /oauth2/token으로 자동 재발급. OAuth 불필요.
  • accountSeq vs accountNo: Toss Gateway는 x-tossinvest-account 헤더에 accountSeq(숫자)를 사용. accountNo(문자열 계좌번호)와 혼용 금지.

Project Information

항목내용
담당자biuea
의존 기능기존 환경·DB·Toss client_credentials 재사용
예상 티켓STK5-01 ~ STK5-07 (총 7개, 4 Wave)

Document History

날짜변경 내용작성자
2026-06-19최초 작성biuea
2026-06-21스킬 포맷 적용biuea

관련 문서