시그널 스냅샷 자정 적재 PRD

배경 (Background)

첫 화면(관심종목·시그널 탭)에 진입하면 WatchlistSection이 마운트 시 관심종목 전체의 시그널을 자동 조회한다. 시그널 1건 계산마다 ML signal-serviceclaude -p(뉴스 번역·감성 분석)를 호출하므로, 관심종목 N개 = 첫 화면 진입마다 claude -p N회가 발생한다. 사용자가 화면을 드나들 때마다 동일 비용이 반복된다.

backend에는 이미 signal_snapshots·recommendation_snapshots·news_snapshots 테이블과 도메인·JPA·PUT/GET 스냅샷 API가 구현돼 있으나, 쓰는 주체도 읽는 주체도 없어 미사용(죽은 계층) 상태다. aggregator에 be.base-url 설정이 박혀 있으나 참조 0건이다. 즉 “스냅샷에 미리 적재하고 캐시에서 서빙한다”는 설계가 의도됐으나 미완성이다.

Problem (AS-IS)

FE(3000) ──► Aggregator(8090) ──► ML(8000) ──► claude -p   (매 조회마다)
                              └─► Redis (장애 시 폴백 전용, 비용 절감 X)
backend signal_snapshots / recommendation_snapshots : writer·reader 없음 (미사용)
  • aggregator SignalDomainService는 항상 ML을 먼저 호출하고, Redis는 ML 장애 시에만 읽는다 → claude 호출을 막지 못한다.
  • aggregator SignalApiController.getSignal은 FE가 보내는 refresh 파라미터를 받지 않는다(무시).
  • ML /signalsrefresh를 무시하고 항상 새로 계산한다.
  • 스냅샷 테이블을 채우는 스케줄러가 없다.

Goal (TO-BE)

자정(00:00 KST) Scheduler ──► ML fresh(claude -p, 1일 1회) ──► backend snapshot(PUT)  [WRITER]

FE 조회 ──► Aggregator ──► Redis(L1) → backend snapshot MySQL(L2) → (miss/refresh)ML(L3)
            refresh=true 일 때만 ML(claude) 강제 호출                                  [READER]
  • 하루 1회(자정) 관심종목·추천을 미리 계산해 MySQL 스냅샷에 적재한다 → 낮 동안 사용자 조회는 캐시 hit로 서빙되어 claude를 호출하지 않는다.
  • 사용자가 “새로고침” 버튼을 누르면(refresh=true)만 최신 ML 계산(claude)을 수행한다.
  • 첫 요청·신규 종목 등 스냅샷이 없는 경우에 한해 ML을 호출하고 즉시 스냅샷에 적재한다.

Requirements

ID요구사항우선순위
R-01aggregator → backend 스냅샷 PUT/GET 호출 게이트웨이(SnapshotGateway) + be.base-url RestClientP0
R-02cache-first 읽기 — getSignal(symbol, refresh): Redis → MySQL 스냅샷 → (miss/refresh) MLP0
R-03getRecommendations(limit, refresh) 동일 cache-firstP0
R-04SignalApiControllerrefresh 파라미터 추가(기본 false), FE 전달값 반영P0
R-05자정(00:00 KST) 스케줄러 — 관심종목 ∪ 추천을 ML fresh 계산 후 스냅샷 적재P0
R-06수동 갱신 트리거 POST /api/v1/snapshots/refresh (멱등)P1
R-07스냅샷·캐시 모두 없고 ML도 실패 시 SignalUnavailableException(기존 동작 유지)P0
R-08스케줄러 부분 실패 격리 — 일부 종목 실패 시 나머지는 적재P1
R-09응답 캐시 출처 표기 — fromCache/X-Cache: HIT 유지P2

사용자 시나리오

시나리오 1 — 첫 화면 비용 0(자정 적재 후) 자정 스케줄러가 관심종목 시그널을 미리 계산해 MySQL에 적재한다. 낮에 사용자가 첫 화면에 진입하면 관심종목 시그널이 MySQL 스냅샷에서 서빙되어 claude 호출이 발생하지 않는다.

시나리오 2 — 수동 새로고침만 최신 계산 사용자가 관심종목 섹션의 “새로고침”을 누르면 refresh=true로 요청이 가고, aggregator가 ML을 호출해 최신 시그널(claude)을 계산한 뒤 스냅샷을 갱신한다.

시나리오 3 — 신규 종목 첫 조회 스냅샷에 없는 종목을 처음 조회하면 ML을 1회 호출하고 즉시 스냅샷에 적재한다. 이후 조회는 캐시 hit.

범위 외

  • backend 스냅샷 도메인·테이블·API 변경 없음 (기존 계층 재사용)
  • ML(signal-service) 코드 변경 없음 (aggregator에서 게이팅)
  • chat(claude -p)의 비용 — 사용자 입력 기반이라 사전 적재 불가, 범위 외
  • 인증/인가 — 현재 없으므로 유지

관련 문서