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.cmd에 bld 통계 id로 POST 시 JSON 응답 |
| bld | KRX 통계 화면 식별자. 통계 종류마다 다른 값 (투자자별 거래실적·공매도 등) |
| OTP 토큰 | bld로 generate.cmd에서 발급받아 getJsonData.cmd에 전달하는 1회용 코드 |
| 투자자별 거래실적 | 개인·외국인·기관(연기금·투신·금투·보험 등) 일별 순매수 |
| EOD | End 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 signal_snapshots 테이블에 영속화 (Redis 제거)
- ML in-memory TtlCache 제거 여부 결정 — ml은 stateless, 영속화는 MySQL로
- CUD 요청의 Toss와 MySQL 처리 순서 결정 — 외부 호출 어댑터는 BE infrastructure에 배치
| 방안 | 설명 | 채택 여부 |
|---|---|---|
| BE(Kotlin) 수집 + MySQL 영속화 + ml read API 소비 | 토스 게이트웨이·신호 영속화와 동일 위치. ml stateless 유지 | 채택 — 기존 ADR 일관, 영속화 책임 BE 집중 |
| ml(Python) 수집 + 자체 저장 | 스크래핑·스코어링이 한 서비스. 구현 단순 | 미채택 — ml stateless 방향 위배, 영속화 이원화 |
| 실시간 스크래핑 | 장중 폴링 | 미채택 — EOD 데이터라 가치 없음, 차단·노이즈 리스크 |
| 도메인 단일 aggregate | 5종을 한 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.duration | timer | 배치 전체 소요 |
marketflow.upsert.duration{statistic} | timer | 벌크 upsert 소요 |
marketflow.collect.failure{statistic} | counter | 통계별 실패·부분실패 |
marketflow.krx.retry | counter | KRX 재시도·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
- DB 마이그레이션(5개 테이블) 선반영 (Flyway).
- KRX MDC client + 팩터별 Gateway·Repository 배포 (스케줄러 비활성 상태).
- 스케줄러 활성화, 1회 수동 트리거로 적재 검증.
- ml 추천 스코어링에 KRX 팩터 반영 배포.
- 롤백: 스케줄러 비활성(플래그 OFF)으로 즉시 수집 중단, 추천은 KRX 팩터 없이 graceful 동작. 테이블은 역방향 DDL로 제거 가능.
Project Information
- 저장소:
backend/(도메인 패키지com.biuea.stock.marketflow),ml/(스코어링 소비) - 포트: backend :8080, ml :8000 (변경 없음)
- 티켓 prefix: STK10