시그널 스냅샷 자정 적재 PRD
배경 (Background)
첫 화면(관심종목·시그널 탭)에 진입하면 WatchlistSection이 마운트 시 관심종목 전체의 시그널을 자동 조회한다. 시그널 1건 계산마다 ML signal-service가 claude -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
/signals도refresh를 무시하고 항상 새로 계산한다. - 스냅샷 테이블을 채우는 스케줄러가 없다.
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-01 | aggregator → backend 스냅샷 PUT/GET 호출 게이트웨이(SnapshotGateway) + be.base-url RestClient | P0 |
| R-02 | cache-first 읽기 — getSignal(symbol, refresh): Redis → MySQL 스냅샷 → (miss/refresh) ML | P0 |
| R-03 | getRecommendations(limit, refresh) 동일 cache-first | P0 |
| R-04 | SignalApiController에 refresh 파라미터 추가(기본 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)의 비용 — 사용자 입력 기반이라 사전 적재 불가, 범위 외 - 인증/인가 — 현재 없으므로 유지