[STK5-01] 공통 DB 스키마 (accounts·orders·holdings·trade_history DDL)

작업 내용 (설계 의도)

변경 사항

Phase 5에서 사용할 4개 테이블을 Flyway 마이그레이션으로 신규 생성한다. STK5-02·STK5-03이 이 스키마를 기반으로 Entity·Repository를 구현하므로, Wave 1에서 단독 완료해야 한다.

각 테이블 설계 의도:

  • toss_tokens: access_token + expires_at 영속화. 앱 재시작 후 토큰 재발급 없이 재사용. 행은 항상 1건(upsert).
  • accounts: 토스 계좌 목록 캐시. selected TINYINT(1)으로 활성 계좌 1개 추적.
  • orders: 주문 이력. toss_order_id UK로 Toss와 MySQL 간 멱등성 보장.
  • holdings: 보유종목 스냅샷. (account_number, symbol) 복합 UK로 upsert.
  • trade_history: 거래내역 스냅샷. toss_transaction_id UK로 중복 적재 방지.

다이어그램

처리 흐름 (Flyway 마이그레이션 적용 순서)

sequenceDiagram
    participant F as Flyway
    participant DB as MySQL(3308)
    F->>DB: V{ts}__create_accounts.sql
    DB-->>F: OK
    F->>DB: V{ts}__create_orders.sql
    DB-->>F: OK
    F->>DB: V{ts}__create_holdings.sql
    DB-->>F: OK
    F->>DB: V{ts}__create_trade_history.sql
    DB-->>F: OK
    Note over DB: 4개 테이블 + UK/인덱스 생성 완료

클래스 의존

flowchart LR
    subgraph Migration["Flyway Migration"]
        M1[accounts DDL]
        M2[orders DDL]
        M3[holdings DDL]
        M4[trade_history DDL]
    end
    subgraph Tables["MySQL Tables"]
        A[(accounts)]
        O[(orders)]
        H[(holdings)]
        T[(trade_history)]
    end
    M1 -->|creates| A
    M2 -->|creates| O
    M3 -->|creates| H
    M4 -->|creates| T
    O -->|account_number ref| A
    H -->|account_number ref| A
    T -->|account_number ref| A

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 ||--o{ ORDERS : "account_number ref"
    ACCOUNTS ||--o{ HOLDINGS : "account_number ref"
    ACCOUNTS ||--o{ TRADE_HISTORY : "account_number ref"
DDL 참고
CREATE TABLE toss_tokens (
    id           BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 ID',
    access_token TEXT NOT NULL COMMENT '토스 access token (JWT)',
    expires_at   DATETIME(6) NOT NULL COMMENT '토큰 만료 시각',
    issued_at    DATETIME(6) NOT NULL COMMENT '발급 시각',
    PRIMARY KEY (id)
) COMMENT = '토스 API 토큰 영속화 (항상 최신 1건)';
 
CREATE TABLE accounts (
    account_number VARCHAR(20) NOT NULL COMMENT '토스 계좌번호',
    account_name   VARCHAR(100) NOT NULL COMMENT '계좌명',
    account_type   VARCHAR(20) NOT NULL COMMENT '계좌 유형 (STOCK/ISA/CMA)',
    cash_balance   DECIMAL(18, 2) NOT NULL DEFAULT 0 COMMENT '예수금',
    selected       TINYINT(1) NOT NULL DEFAULT 0 COMMENT '활성 계좌 여부 (1개만 1)',
    refreshed_at   DATETIME(6) NOT NULL COMMENT '마지막 동기화 시각',
    created_at     DATETIME(6) NOT NULL COMMENT '최초 등록 시각',
    PRIMARY KEY (account_number)
) COMMENT = '토스 계좌 캐시';
 
CREATE TABLE orders (
    id             BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 주문 ID',
    toss_order_id  VARCHAR(50) NOT NULL COMMENT '토스 발급 주문 ID',
    account_number VARCHAR(20) NOT NULL COMMENT '주문 계좌번호',
    symbol         VARCHAR(20) NOT NULL COMMENT '종목 코드',
    order_type     VARCHAR(10) NOT NULL COMMENT '주문 방향 (BUY/SELL)',
    price_type     VARCHAR(10) NOT NULL COMMENT '가격 유형 (LIMIT/MARKET)',
    quantity       INT NOT NULL COMMENT '주문 수량',
    price          DECIMAL(18, 2) NOT NULL COMMENT '주문 가격',
    status         VARCHAR(20) NOT NULL COMMENT '주문 상태 (PENDING/COMPLETED/CORRECTED/CANCELLED)',
    ordered_at     DATETIME(6) NOT NULL COMMENT '주문 접수 시각',
    corrected_at   DATETIME(6) NULL COMMENT '정정 시각',
    cancelled_at   DATETIME(6) NULL COMMENT '취소 시각',
    PRIMARY KEY (id),
    UNIQUE KEY uk_toss_order_id (toss_order_id),
    INDEX idx_account_number (account_number),
    INDEX idx_status (status)
) COMMENT = '주문 이력';
 
CREATE TABLE holdings (
    id             BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 ID',
    account_number VARCHAR(20) NOT NULL COMMENT '계좌번호',
    symbol         VARCHAR(20) NOT NULL COMMENT '종목 코드',
    quantity       INT NOT NULL COMMENT '보유 수량',
    average_price  DECIMAL(18, 2) NOT NULL COMMENT '평균 매수가',
    refreshed_at   DATETIME(6) NOT NULL COMMENT '마지막 동기화 시각',
    PRIMARY KEY (id),
    UNIQUE KEY uk_account_symbol (account_number, symbol)
) COMMENT = '보유 종목 스냅샷';
 
CREATE TABLE trade_history (
    id                    BIGINT NOT NULL AUTO_INCREMENT COMMENT '내부 ID',
    toss_transaction_id   VARCHAR(50) NOT NULL COMMENT '토스 거래 ID',
    account_number        VARCHAR(20) NOT NULL COMMENT '계좌번호',
    symbol                VARCHAR(20) NOT NULL COMMENT '종목 코드',
    trade_type            VARCHAR(10) NOT NULL COMMENT '거래 유형 (BUY/SELL)',
    quantity              INT NOT NULL COMMENT '거래 수량',
    price                 DECIMAL(18, 2) NOT NULL COMMENT '체결가',
    traded_at             DATETIME(6) NOT NULL COMMENT '체결 시각',
    PRIMARY KEY (id),
    UNIQUE KEY uk_toss_transaction_id (toss_transaction_id),
    INDEX idx_account_traded (account_number, traded_at)
) COMMENT = '거래 내역 스냅샷';

롤백: DROP TABLE trade_history; DROP TABLE holdings; DROP TABLE orders; DROP TABLE accounts;

테스트 케이스

  • Flyway 마이그레이션 4개 스크립트가 로컬 MySQL 8.0에서 오류 없이 순서대로 실행된다.
  • orders 테이블에 동일 toss_order_id 2건을 INSERT하면 Duplicate Key 에러가 발생한다.
  • holdings 테이블에 동일 (account_number, symbol) 조합으로 UPSERT 2회 시 행이 1건으로 유지되고 refreshed_at이 최신값으로 갱신된다.
  • trade_history 테이블에 동일 toss_transaction_id 2건을 INSERT하면 Duplicate Key 에러가 발생한다.
  • accounts.selected = 1인 행이 0개인 상태에서도 스키마 오류 없이 조회가 가능하다 (DB 레벨 강제 아님, 애플리케이션 제어).