KRX 수급 데이터 연동 TDD

Background

PRD 참조. 토스 Open API에 없는 외국인·기관 수급 등 시장 미시구조 데이터를 KRX(data.krx.co.kr)에서 EOD로 수집해 추천 산정 팩터로 제공한다.

Overview

  • backend/(Kotlin, Hexagonal)에 marketflow 도메인 패키지를 신설한다.
  • KRX MDC 호출은 infrastructure/krx의 어댑터로 캡슐화한다 (토스가 infrastructure/toss인 것과 동일 구조).
  • 마감 후 1일 1배치 스케줄러가 투자자별 거래실적·외국인 보유·밸류에이션·공매도·신용을 수집해 MySQL에 upsert한다.
  • 스케줄러와 별개로 수동 갱신 API(POST /market-flow/collect)가 동일 수집 UseCase를 호출한다 (멱등). 자동·수동 두 진입점 모두 presentation에 위치.
  • presentation의 read API가 종목·기준일별 수급 팩터를 노출하고, ml 추천 스코어링이 이를 소비한다 (ml은 stateless 유지).
  • 적재는 통계당 JDBC 벌크 upsert(INSERT … ON DUPLICATE KEY UPDATE)로 처리한다 (전종목 ~2,800행/통계, 행 단위 save 금지).
  • 배치 run 결과를 marketflow_collect_log에 남기고, 실패 시 Discord 알림 + Micrometer 메트릭(행수·소요·재시도)을 계측한다 — 청크 전환 시그널 관측용.

Terminology

용어정의
KRX MDC정보데이터시스템(data.krx.co.kr). getJsonData.cmdbld 통계 id로 POST 시 JSON 응답
bldKRX 통계 화면 식별자. 통계 종류마다 다른 값 (투자자별 거래실적·공매도 등)
OTP 토큰bld로 generate.cmd에서 발급받아 getJsonData.cmd에 전달하는 1회용 코드
투자자별 거래실적개인·외국인·기관(연기금·투신·금투·보험 등) 일별 순매수
EODEnd Of Day. 장 마감 기준 확정 통계
marketflow신설 도메인 패키지. 일별 종목 수급·미시구조 스냅샷

Define Problem

AS-IS

추천 스코어링 입력 = 기술 모멘텀 + 뉴스 감성
- 외국인·기관 수급 데이터 없음 (토스 API 미제공)
- 밸류에이션·공매도·신용 데이터 없음
→ 중기 시그널 근거 부족

TO-BE

backend/marketflow (Kotlin, Hexagonal)
  presentation   MarketFlowApiController (read), MarketFlowCollectScheduler
  application    CollectMarketFlowUseCase, GetMarketFlowUseCase
  domain         InvestorTrading / ForeignHolding / Valuation / ShortSelling / CreditBalance
                 *Repository, *Gateway (interface)
  infrastructure krx/  KrxMdcClient + Krx*GatewayImpl  (OTP·retry·backoff)
                 mysql/ *RepositoryImpl + JPA

마감 후 1배치 → 5종 수집 → upsert → read API → ml 추천 스코어링 소비

Possible Solutions

과거 결정 참조

방안설명채택 여부
BE(Kotlin) 수집 + MySQL 영속화 + ml read API 소비토스 게이트웨이·신호 영속화와 동일 위치. ml stateless 유지채택 — 기존 ADR 일관, 영속화 책임 BE 집중
ml(Python) 수집 + 자체 저장스크래핑·스코어링이 한 서비스. 구현 단순미채택 — ml stateless 방향 위배, 영속화 이원화
실시간 스크래핑장중 폴링미채택 — EOD 데이터라 가치 없음, 차단·노이즈 리스크
도메인 단일 aggregate5종을 한 Entity에미채택 — 테이블·수집주기·시차가 달라 팩터별 분리가 단순

Detail Design

Component Diagram

flowchart LR
    subgraph Presentation
        Scheduler[MarketFlowCollectScheduler]
        CollectCtrl[MarketFlowCollectApiController]
        Controller[MarketFlowApiController]
    end
    subgraph Application
        CollectUC[CollectMarketFlowUseCase]
        GetUC[GetMarketFlowUseCase]
    end
    subgraph Domain
        DS[MarketFlowDomainService]
        Repo[(MarketFlow Repositories)]
        Gw[MarketFlow Gateways]
    end
    subgraph Infrastructure
        Krx[KrxMdcClient + Krx GatewayImpl]
        Mysql[RepositoryImpl JPA]
    end
    Scheduler --> CollectUC
    CollectCtrl --> CollectUC
    Controller --> GetUC
    CollectUC --> DS
    GetUC --> DS
    DS --> Repo
    DS --> Gw
    Krx -.implements.-> Gw
    Mysql -.implements.-> Repo

Sequence Diagram — 수집 (자동 스케줄러 / 수동 트리거 공통)

sequenceDiagram
    participant Sched as Scheduler / ManualTrigger
    participant UC as CollectMarketFlowUseCase
    participant DS as MarketFlowDomainService
    participant Gw as Krx GatewayImpl
    participant Krx as KrxMdcClient
    participant Repo as RepositoryImpl
    Sched->>UC: execute(baseDate)
    UC->>DS: collect(baseDate)
    loop 5종 통계
        DS->>Gw: fetch(baseDate)
        Gw->>Krx: getJsonData(bld, params)
        Krx-->>Gw: rows
        Gw-->>DS: domain models
        DS->>Repo: upsertAll(models)
    end
    DS-->>UC: collected summary
    UC-->>Sched: result

Sequence Diagram — 추천 스코어링 소비

sequenceDiagram
    participant ML as ml scoring
    participant Ctrl as MarketFlowApiController
    participant UC as GetMarketFlowUseCase
    participant DS as MarketFlowDomainService
    ML->>Ctrl: GET /market-flow?symbol&baseDate
    Ctrl->>UC: execute(query)
    UC->>DS: findBy(symbol, baseDate)
    DS-->>UC: factors
    UC-->>Ctrl: response
    Ctrl-->>ML: factors json
    Note over ML: 팩터 없으면 가중 제외 graceful

ERD

erDiagram
    krx_investor_trading {
        bigint id PK
        varchar symbol
        date base_date
        bigint foreign_net
        bigint institution_net
        bigint individual_net
    }
    krx_foreign_holding {
        bigint id PK
        varchar symbol
        date base_date
        bigint holding_shares
        decimal holding_ratio
        decimal limit_exhaustion_ratio
    }
    krx_valuation {
        bigint id PK
        varchar symbol
        date base_date
        decimal per
        decimal pbr
        decimal dividend_yield
    }
    krx_short_selling {
        bigint id PK
        varchar symbol
        date trade_date
        date disclosed_date
        bigint short_volume
        decimal short_balance_ratio
    }
    krx_credit_balance {
        bigint id PK
        varchar symbol
        date base_date
        bigint credit_balance
        decimal credit_ratio
    }
    marketflow_collect_log {
        bigint id PK
        date base_date
        varchar statistic
        varchar status
        int row_count
        int duration_ms
        varchar error_message
        datetime created_at
    }
  • 각 팩터 테이블 (symbol, base_date) unique 제약으로 upsert 멱등 보장 (공매도는 (symbol, trade_date)).
  • marketflow_collect_log는 run별·통계별 1행 — 상태(완료/실패/부분실패)·행수·소요·에러를 기록. 관측 스택과 무관하게 동작하는 실행 이력 원천.
  • FK 컬럼 미사용 (애플리케이션 레벨 관리), DATE/DATETIME(6) 정밀도 준수, BOOLEAN 미사용(status는 VARCHAR).

Testing Plan

  • KrxMdcClient: OTP 발급→getJsonData 흐름, 4xx/5xx 시 재시도·백오프, 차단 응답 처리 (TestContainers 불필요, WireMock/MockWebServer)
  • Krx*GatewayImpl: KRX JSON → 도메인 모델 파싱, 빈 응답·필드 누락 graceful
  • 각 *RepositoryImpl: upsert 멱등(동일 symbol·base_date 재적재), 통합(TestContainers MySQL)
  • MarketFlowDomainService: 부분 실패 시 나머지 적재 지속, 공매도 거래일/공시일 구분
  • CollectMarketFlowUseCase: 5종 수집 오케스트레이션, 일부 실패 허용
  • 적재: 통계당 1회 벌크 upsert로 ~2,800행 멱등 처리 (행 단위 save 미사용 검증)
  • 모니터링: run 결과가 collect_log에 기록, 실패·부분실패 시 Discord 알림 호출, 메트릭 카운터·타이머 증가
  • MarketFlowApiController: 종목·기준일 조회, 미수집 시 빈 응답
  • scenario: 마감 후 수집 → read API 조회 → 팩터 반환 E2E

Observability

기존 옵저버빌리티 스택(PRD)에 정렬한다 — OTel(OTLP) → 단일 Collector → Grafana(Prometheus/Loki). backend는 OTel Java agent 자동계측이라 Micrometer 메터가 그대로 export된다.

실행 이력·알림 (관측 스택 비의존)

  • marketflow_collect_log에 run별·통계별 상태·행수·소요·에러 기록.
  • 실패·부분 실패 시 DiscordNotificationGateway(alert 도메인)로 즉시 알림 (마감 후 배치).

메트릭 (청크 전환 시그널)

메트릭타입의미
marketflow.collect.rows{statistic}gauge통계별 수집 행수
marketflow.collect.duration{statistic}timer통계별 소요
marketflow.collect.total.durationtimer배치 전체 소요
marketflow.upsert.duration{statistic}timer벌크 upsert 소요
marketflow.collect.failure{statistic}counter통계별 실패·부분실패
marketflow.krx.retrycounterKRX 재시도·429 횟수

JVM heap은 OTel agent 자동계측 메트릭을 그대로 활용한다.

전환 임계(알람)

  • 단일 통계 행수 > 50,000, 전체 소요 > 5분, 단일 통계 > 2분, 벌크 upsert > 30초, heap 사용률 급증/OOM 근접, 부분 실패·429 빈발.
  • 임계 지속 초과 시 Grafana 알람 → Discord. 이것이 Spring Batch(청크·restart·partition) 전환 신호다.
  • 대시보드·알람 규칙은 옵저버빌리티 스택(STK-OBS) 완료에 의존. 메트릭 emission·DB 로그·Discord 알림은 선행 무관하게 동작.

Release Scenario

  1. DB 마이그레이션(5개 테이블) 선반영 (Flyway).
  2. KRX MDC client + 팩터별 Gateway·Repository 배포 (스케줄러 비활성 상태).
  3. 스케줄러 활성화, 1회 수동 트리거로 적재 검증.
  4. ml 추천 스코어링에 KRX 팩터 반영 배포.
  5. 롤백: 스케줄러 비활성(플래그 OFF)으로 즉시 수집 중단, 추천은 KRX 팩터 없이 graceful 동작. 테이블은 역방향 DDL로 제거 가능.

Project Information

  • 저장소: backend/ (도메인 패키지 com.biuea.stock.marketflow), ml/ (스코어링 소비)
  • 포트: backend :8080, ml :8000 (변경 없음)
  • 티켓 prefix: STK10