주식 추천 고도화 TDD

Background

현재 stock-application은 3개 서비스로 구성된다.

  • backend (Spring Boot, :8080): 실제 비즈니스 도메인 — 계좌/주문/보유/관심종목/알림
  • aggregator (Spring Boot, :8090): BFF — backend + ML 응답을 조합해 프런트에 제공
  • ml (FastAPI, :8000): Python — 종목 신호(Signal), 예측(Prediction), 추천(Recommendation)

현재 LLM 호출 흐름과 문제:

Aggregator → ML /signals/{symbol}
               └─ TtlCache HIT(10분) → 즉시 반환
               └─ TtlCache MISS → Claude CLI subprocess 실행 (LLM 호출)
             → 결과를 Aggregator Redis에도 저장 (10분/5분 TTL)

TTL 만료 = 자동 LLM 호출. 추천 12종목 × 10분마다 = 최대 12회/10분.

Overview

구성 요소변경 내용
backendportfolio 도메인 신설 (ManualHolding CRUD + 손익 계산), signal 도메인 신설 (신호 스냅샷 영속화)
ml/portfolio-forecast/{symbol} 신설 (단기/중기/장기 예측 + 매도 추천), TtlCache 제거
aggregator포트폴리오 분석 API + 추천 확장 API 신설, Redis → Backend DB 교체, refresh 로직 추가

Terminology

용어정의
ManualHolding사용자가 직접 입력한 수동 포트폴리오 항목 (종목·매수가·수량)
Holding토스 API에서 가져오는 실계좌 보유 종목 스냅샷 (기존, 변경 없음)
SignalSnapshotML이 특정 시점에 계산한 Signal/Prediction 결과를 MySQL에 저장한 레코드
horizon예측 기간(주 단위) — 단기(2주), 중기(8주), 장기(26주)
손익(현재가 − 평균 매수가) × 보유 수량
수익률(현재가 − 평균 매수가) / 평균 매수가 × 100 (%)
SellRecommendationhorizon별 최적 매도 시점 + 예상 가격 + 예상 손익
새로고침사용자가 명시적으로 ML 재호출을 요청하는 행위

Define Problem

AS-IS

  • ML 예측은 5일(고정) 단기 예측만 제공 — PredictionPoint(date, price, upper, lower) D+1..D+5
  • 사용자가 “내 매수가”를 기록하는 수단이 없음
  • 추천 종목(Recommendation)에 예측 데이터가 없음 — entry 진입 제안만 있음
  • ML in-memory TtlCache(10분) + Aggregator Redis(10분) 이중 캐시 — TTL 만료 시 자동 LLM 재호출

TO-BE

  • ML: horizon 파라미터로 단기(14일)/중기(56일)/장기(182일) 예측 + 주 단위 집계 + 매도 추천 산출, TtlCache 제거
  • Backend: manual_holdings 테이블(포트폴리오) + signal_snapshots 테이블(신호 영속화) 신설
  • Aggregator: Redis 제거, Backend DB 먼저 조회 → MISS/refresh 시 ML 호출, 포트폴리오/추천 API 신설

Possible Solutions

방안 비교 — 장기 예측 모델

방안설명왜 채택미채택 대안
선형 외삽 확장기존 build_prediction의 slope 기반 예측을 horizon=182까지 연장추가 데이터 필요 없음, 현재 ML 구조에 자연스럽게 확장
ARIMA/Prophet통계 시계열 모델정확도 향상 가능데이터 수집 기간 필요, 과적합 위험, 운영 복잡도 증가 → 미채택
ML(LSTM)딥러닝 기반최고 정확도 가능성GPU 필요, 학습 데이터 1년 이상 필요 → 미채택

sigma 확산은 sigma × sqrt(d) 적용으로 장기 불확실성을 표현한다.

방안 비교 — 수동 포트폴리오 저장

방안설명왜 채택미채택 대안
Backend DBmanual_holdings 테이블 신설, JPA 영속화기존 도메인 패턴과 일관성
프런트 LocalStorage브라우저에만 저장서버 불필요디바이스 이동 불가, 데이터 유실 위험 → 미채택

방안 비교 — 신호 영속화 위치

방안설명왜 채택미채택 대안
Backend MySQL 경유Aggregator → Backend API → MySQL기존 패턴 일관성, 단일 DB 소유 주체
Aggregator 직접 MySQLAggregator에 JPA 추가네트워크 홉 감소아키텍처 일관성 훼손 → 미채택
Redis TTL 대폭 확대TTL을 24h로 늘림코드 변경 최소재시작 시 초기화, 영속성 없음, 근본 해결 아님 → 미채택

Detail Design

AS-IS / TO-BE 비교

항목AS-ISTO-BE
LLM 호출 시점TTL 10분 만료 시 자동사용자 새로고침 요청 시만
신호 캐시 레이어ML in-memory + Redis (이중)MySQL 단일 영속화
예측 horizonD+1..D+5 (5일 고정)단기 14일 / 중기 56일 / 장기 182일, 주 단위 집계
수동 포트폴리오없음manual_holdings 테이블, CRUD API
추천 종목 예측진입가 제안만단기/중기/장기 예측 + 매도 추천 포함
손익 계산없음(현재가 − 평균 매수가) × 수량

Component Diagram

flowchart LR
    subgraph Frontend["Frontend"]
        PortfolioUI["Portfolio UI"]
        RecommendUI["Recommend UI"]
    end
    subgraph Aggregator["Aggregator :8090"]
        SignalCtrl["SignalApiController (refresh 추가)"]
        PortfolioCtrl["ManualHoldingApiController"]
        SignalDS["SignalDomainService (DB 우선)"]
        SnapshotRepo["BeSignalSnapshotRepositoryImpl"]
        AnalysisUC["GetPortfolioAnalysisUseCase"]
    end
    subgraph Backend["Backend :8080"]
        PortfolioAPI["ManualHoldingApiController"]
        SnapshotAPI["SignalSnapshotApiController"]
        DB[("MySQL")]
    end
    subgraph ML["ML :8000"]
        SignalAPI["/signals/{symbol}"]
        ForecastAPI["/portfolio-forecast/{symbol}"]
    end
    PortfolioUI --> PortfolioCtrl
    RecommendUI --> SignalCtrl
    PortfolioCtrl --> AnalysisUC
    SignalCtrl --> SignalDS
    SignalDS --> SnapshotRepo
    SignalDS --> SignalAPI
    SnapshotRepo --> SnapshotAPI
    AnalysisUC --> PortfolioAPI
    AnalysisUC --> ForecastAPI
    PortfolioAPI --> DB
    SnapshotAPI --> DB

Sequence Diagram — 신호 조회 (DB HIT)

sequenceDiagram
    participant FE as Frontend
    participant AGG as Aggregator
    participant BE as Backend
    FE->>AGG: GET /api/v1/signals/{symbol}
    AGG->>BE: GET /signal-snapshots/{symbol}
    BE-->>AGG: SignalSnapshotResponse
    AGG-->>FE: SignalResponse (fromCache: true)

Sequence Diagram — 신호 새로고침

sequenceDiagram
    participant FE as Frontend
    participant AGG as Aggregator
    participant BE as Backend
    participant ML as ML
    FE->>AGG: GET /api/v1/signals/{symbol}?refresh=true
    AGG->>ML: GET /signals/{symbol}
    ML-->>AGG: 신규 계산 결과 (Claude CLI 실행)
    AGG->>BE: PUT /signal-snapshots/{symbol}
    AGG-->>FE: SignalResponse (fromCache: false)

Sequence Diagram — 포트폴리오 분석 조회

sequenceDiagram
    participant FE as Frontend
    participant AGG as Aggregator
    participant BE as Backend
    participant ML as ML
    FE->>AGG: GET /api/v1/portfolio/{symbol}/analysis
    par 병렬 호출
        AGG->>BE: GET /manual-holdings/{symbol}/pnl
        BE-->>AGG: ManualHoldingWithPnlResponse
    and
        AGG->>ML: GET /portfolio-forecast/{symbol}
        ML-->>AGG: PortfolioForecastResult
    end
    AGG-->>FE: PortfolioAnalysisResponse

ERD

erDiagram
    MANUAL_HOLDINGS {
        bigint id PK
        varchar symbol "종목 코드 (UNIQUE)"
        varchar name "종목명"
        decimal avg_price "평균 매수가"
        int quantity "보유 수량"
        datetime created_at
        datetime updated_at
    }
    SIGNAL_SNAPSHOTS {
        bigint id PK
        varchar symbol "종목 코드 (UNIQUE)"
        text signal_json "SignalResult JSON"
        text prediction_json "PredictionResult JSON (nullable)"
        datetime refreshed_at "마지막 ML 호출 시각"
        datetime created_at
        datetime updated_at
    }
    RECOMMENDATION_SNAPSHOTS {
        bigint id PK
        varchar snapshot_key "고정값 default (UNIQUE)"
        text recommendations_json "RecommendationResult JSON"
        datetime refreshed_at
        datetime created_at
        datetime updated_at
    }

Testing Plan

  • ManualHolding 손익 계산 및 symbol 유일성 검증 — domain 단위 테스트
  • SignalSnapshot 생성·갱신 — domain 단위 테스트
  • AddManualHoldingUseCase, ListManualHoldingsUseCase, SaveSignalSnapshotUseCase — application 단위 테스트 (DomainService mock)
  • ManualHoldingRepositoryImpl, SignalSnapshotRepositoryImpl — infrastructure 통합 테스트 (Testcontainers MySQL)
  • ManualHoldingApiController, SignalSnapshotApiController — presentation 통합 테스트 (MockMvc)
  • ML build_portfolio_forecast 단기(2주)/중기(8주)/장기(26주) 포인트 수 검증 — pytest 단위 테스트
  • SignalDomainService refresh=false/true 분기 — aggregator domain 단위 테스트 (Gateway·Repository mock)
  • BeSignalSnapshotRepositoryImpl — aggregator infra 통합 테스트 (WireMock)

핵심 시나리오:

  • DB에 스냅샷 있고 refresh=false → ML 호출 없이 DB 반환
  • DB에 스냅샷 없고 refresh=false → ML 호출 → DB 저장 → 반환
  • refresh=true → ML 항상 호출 → DB 갱신 → 반환
  • ML 다운 + DB 스냅샷 있음 → DB 결과 반환 (폴백)
  • 포트폴리오 손익: 현재가 > 매수가이면 양수, 현재가 < 매수가이면 음수
  • 중복 symbol 추가 시 예외 발생

Release Scenario

  1. DB 마이그레이션 먼저 적용 (manual_holdings, signal_snapshots, recommendation_snapshots)
  2. Backend 배포 (ManualHolding CRUD + SignalSnapshot API)
  3. ML 배포 (portfolio-forecast endpoint + TtlCache 제거)
  4. Aggregator 배포 (Redis → DB 교체, 포트폴리오 분석 API, refresh 로직)
  5. Frontend 배포
  6. Redis 연결 설정 제거 (인프라 정리)

롤백: Aggregator 이전 버전으로 교체하면 Redis 폴백으로 즉시 복구. signal_snapshots 테이블은 그대로 둬도 무방.

Project Information

항목내용
프로젝트stock-application
과제주식 추천 고도화
담당자biuea
Jira EpicSTK8, STK9
총 티켓9개 (Wave 1~4)

Document History

날짜변경 내용작성자
2026-06-20최초 작성 (포트폴리오 + 예측 고도화)biuea
2026-06-20신호 캐시 영속화 내용 통합 (Redis → MySQL, refresh 로직)biuea
2026-06-21스킬 포맷 정비 (클래스 역할 정의 섹션 제거, Testing Plan bullet 변환, Project Information 추가)biuea