[WATCH-03] BE: 뉴스 스냅샷 영속화 (DB + UseCase + API)

작업 내용 (설계 의도)

변경 사항

news_snapshots 테이블을 추가하고 watchlist 도메인에 뉴스 스냅샷 저장/조회 UseCase 및 API를 추가한다. FE는 GET /api/v1/watchlist/{symbol}/news로 영속화된 뉴스 요약과 헤드라인을 조회한다. 스냅샷이 1시간 이상 stale이거나 없으면 ML을 호출해 갱신한다. WATCH-01이 완료되어 ML의 /signals 응답에 headlines(list[Headline])·news_summary가 포함된 이후 진행한다.

DDL 참고
CREATE TABLE news_snapshots (
    id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'PK',
    symbol VARCHAR(20) NOT NULL COMMENT '종목 심볼',
    summary TEXT NOT NULL COMMENT '뉴스 전체 요약',
    headlines JSON NOT NULL COMMENT '헤드라인 배열 [{title,url,translated_title,sentiment}]',
    refreshed_at DATETIME(6) NOT NULL COMMENT '마지막 갱신 시각',
    created_at DATETIME(6) NOT NULL COMMENT '생성 시각',
    UNIQUE KEY uq_symbol (symbol)
) COMMENT = '관심종목 뉴스 스냅샷 캐시';

다이어그램

처리 흐름

sequenceDiagram
    participant FE
    participant BE as WatchlistApiController
    participant UCA as GetNewsSnapshotUseCase
    participant DS as NewsSnapshotDomainService
    participant ML as ML :8000
    FE->>BE: GET /api/v1/watchlist/{symbol}/news
    BE->>UCA: execute(symbol)
    UCA->>DS: getOrRefresh(symbol)
    alt 캐시 신선 (1시간 이내)
        DS-->>UCA: NewsSnapshot
    else stale / 없음
        DS->>ML: GET /signals/{symbol}
        ML-->>DS: Signal
        DS->>DS: save(NewsSnapshot)
        DS-->>UCA: NewsSnapshot
    end
    UCA-->>BE: NewsSnapshotResponse
    BE-->>FE: 200 OK

클래스 의존

flowchart LR
    WatchlistApiController --> GetNewsSnapshotUseCase
    WatchlistApiController --> SaveNewsSnapshotUseCase
    GetNewsSnapshotUseCase --> NewsSnapshotDomainService
    SaveNewsSnapshotUseCase --> NewsSnapshotDomainService
    NewsSnapshotDomainService --> NewsSnapshotRepository
    NewsSnapshotRepository -.->|implements| NewsSnapshotRepositoryImpl
    NewsSnapshotDomainService --> SignalGateway
    SignalGateway -.->|implements| SignalGatewayImpl

테스트 케이스

  • NewsSnapshotDomainService.isStale이 1시간 초과 스냅샷을 stale로 판단한다
  • NewsSnapshotDomainService.isStale이 59분 스냅샷을 신선으로 판단한다
  • GetNewsSnapshotUseCase가 신선한 캐시를 ML 호출 없이 반환한다
  • GetNewsSnapshotUseCase가 stale 캐시를 ML 재호출 후 갱신해 반환한다
  • ML 호출 실패 시 기존 stale 스냅샷을 그대로 반환한다 (graceful degradation)
  • SaveNewsSnapshotUseCase가 신규 스냅샷을 upsert 저장한다
  • NewsSnapshotRepositoryImpl이 symbol 기준 upsert를 올바르게 처리한다 (Testcontainers)
  • GET /api/v1/watchlist/{symbol}/news{summary, headlines, refreshedAt} 를 반환한다
  • watchlist에 없는 symbol 조회 시 404를 반환한다