추천 종목 고도화 TDD

Background

PRD 참조. 기존에 구현한 대형주 한정 추천 시스템을 중·소형주 확대, 테마 연관 종목 추천, 관심종목 연동, SSE 실시간 가격 전송으로 고도화한다.

Overview

  • ML 서비스(Python :8000): CURATED_SYMBOLS 확장, GET /theme-recommendations 신규
  • BE(Kotlin/Spring :8080): ListStocksUseCase watchlist 연동, SseEmitterRegistry payload 변경

Terminology

용어설명
CURATED_SYMBOLSML 서비스 추천 후보군 하드코딩 목록
테마 연관 추천뉴스 호재 테마 → 연관 종목 체인 추천
SSE payloadServer-Sent Events 이벤트 data 필드
isInWatchlist종목 응답에서 관심종목 등록 여부

Define Problem

AS-IS

  • ML: CURATED_SYMBOLS = 대형주 KR 8 + US 4 고정
  • ML: 테마 연관 추천 없음
  • BE ListStocksUseCase: watchlist 미참조 → isInWatchlist 항상 false
  • BE SseEmitterRegistry.broadcastRefresh(): "refresh" 문자열만 전송 → FE가 REST 추가 호출

TO-BE

  • ML: CURATED_SYMBOLS dict 확장 (large/mid/small 카테고리), category 필터 파라미터
  • ML: theme_chain.py + GET /theme-recommendations 신규
  • BE ListStocksUseCase: WatchlistDomainService.findAll() 교차 조회
  • BE SseEmitterRegistry.broadcast(prices): 가격 JSON 배열 전송

Possible Solutions

방안설명채택미채택 사유
SSE refresh 유지 + FE REST 추가 호출현재 구조 유지불필요한 왕복 1회
SSE payload에 가격 데이터 포함broadcastRefresh → broadcast(prices)
정적 테마 매핑만Python dict로 테마→종목 하드코딩✅ (v1)v2에서 Claude 보완
WatchlistRepository 직접 주입stock UseCase에서 watchlist Repository 직접 참조도메인 경계 위반(p1)
WatchlistDomainService 주입UseCase → DomainService 경유컨벤션 준수

Detail Design

AS-IS / TO-BE 비교

ListStocksUseCase

# AS-IS
execute(page, size):
  (stocks, total) = stockDomainService.listStocks(page, size)
  → isInWatchlist = false (항상)

# TO-BE
execute(page, size):
  (stocks, total) = stockDomainService.listStocks(page, size)
  watchlistSymbols = watchlistDomainService.findAll().map { it.symbol }.toSet()
  → isInWatchlist = stock.symbol in watchlistSymbols

SseEmitterRegistry

# AS-IS
broadcastRefresh():
  event = SseEmitter.event().name("price-updated").data("refresh")
  emitters.forEach { it.send(event) }

# TO-BE
broadcast(prices: List<StockPriceCache>):
  payload = objectMapper.writeValueAsString(prices.map { PricePayload(it) })
  event = SseEmitter.event().name("price-updated").data(payload)
  emitters.forEach { it.send(event) }

StockPriceScheduler

# AS-IS
syncAndBroadcast():
  syncStocksUseCase.execute()
  sseEmitterRegistry.broadcastRefresh()

# TO-BE
syncAndBroadcast():
  prices = syncStocksUseCase.executeAndReturn()   // 또는 별도 조회
  sseEmitterRegistry.broadcast(prices)

Component Diagram

flowchart LR
    subgraph Presentation["Presentation"]
        StockController["StockApiController"]
        Scheduler["StockPriceScheduler"]
    end
    subgraph Application["Application"]
        ListUC["ListStocksUseCase"]
        SyncUC["SyncStocksUseCase"]
        SseReg["SseEmitterRegistry"]
    end
    subgraph Domain["Domain (stock)"]
        StockDS["StockDomainService"]
    end
    subgraph WatchlistDomain["Domain (watchlist)"]
        WatchlistDS["WatchlistDomainService"]
    end
    StockController --> ListUC
    ListUC --> StockDS
    ListUC --> WatchlistDS
    Scheduler --> SyncUC
    Scheduler --> SseReg
    SyncUC --> StockDS

Sequence Diagram — SSE 가격 전송

sequenceDiagram
    participant Sch as StockPriceScheduler
    participant UC as SyncStocksUseCase
    participant DS as StockDomainService
    participant Cache as StockPriceCacheRepo
    participant Sse as SseEmitterRegistry
    participant FE as FE(SSE Client)

    Sch->>UC: executeAndReturn()
    UC->>DS: syncPrices()
    DS->>Cache: upsertAll(prices)
    DS-->>UC: prices: List<StockPriceCache>
    UC-->>Sch: prices
    Sch->>Sse: broadcast(prices)
    Sse->>FE: SSE event(price-updated, JSON[])

Sequence Diagram — 테마 연관 추천 (ML)

sequenceDiagram
    participant C as FE/Client
    participant ML as ML Service
    participant TH as theme_chain.py
    participant Map as THEME_MAPPING

    C->>ML: GET /theme-recommendations
    ML->>TH: extract_theme(recent_headlines)
    TH-->>ML: theme = "AI_SERVER"
    ML->>Map: get_related_stocks("AI_SERVER")
    Map-->>ML: [{symbol, name, relation}, ...]
    ML-->>C: [{theme, reason, stocks}]

ERD

변경 없음 — DB 스키마 변경 없다. watchlist·stock 테이블 재사용.

Testing Plan

  • ML: get_curated_symbols(category) — large/mid/small/all 각 카테고리별 심볼 수 검증
  • ML: extract_theme(headlines) — AI_SERVER 키워드 감지·빈 헤드라인 graceful 처리 검증
  • ML: GET /theme-recommendations — 200 응답, [{theme, reason, stocks}] 형식 준수 검증
  • BE: ListStocksUseCase — 관심종목 교차 조회 후 isInWatchlist 정확성 + 예외 시 graceful 검증
  • BE: SseEmitterRegistry.broadcast(prices) — JSON 배열 event 전송·실패 emitter 제거·빈 목록 처리 검증

Release Scenario

  1. ML 서비스 배포: CURATED_SYMBOLS 확장 + theme_chain.py 추가
  2. BE 배포: ListStocksUseCase, SseEmitterRegistry, StockPriceScheduler 변경
  3. FE 변경: SSE data 파싱 로직 (JSON 배열 처리), 관심종목 버튼 로직 확인

롤백: ML 서비스는 구버전 재배포 (category 파라미터 없으면 all 기본값 유지). BE는 broadcastRefresh() 메서드 복구.

Project Information

항목내용
담당biuea
브랜치 접두사feat/STK7-0N-*
베이스 브랜치main

Document History

날짜변경 내용작성자
2026-06-20초안 작성biuea