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
구성 요소
변경 내용
backend
portfolio 도메인 신설 (ManualHolding CRUD + 손익 계산), signal 도메인 신설 (신호 스냅샷 영속화)
ml
/portfolio-forecast/{symbol} 신설 (단기/중기/장기 예측 + 매도 추천), TtlCache 제거
aggregator
포트폴리오 분석 API + 추천 확장 API 신설, Redis → Backend DB 교체, refresh 로직 추가
Terminology
용어
정의
ManualHolding
사용자가 직접 입력한 수동 포트폴리오 항목 (종목·매수가·수량)
Holding
토스 API에서 가져오는 실계좌 보유 종목 스냅샷 (기존, 변경 없음)
SignalSnapshot
ML이 특정 시점에 계산한 Signal/Prediction 결과를 MySQL에 저장한 레코드
horizon
예측 기간(주 단위) — 단기(2주), 중기(8주), 장기(26주)
손익
(현재가 − 평균 매수가) × 보유 수량
수익률
(현재가 − 평균 매수가) / 평균 매수가 × 100 (%)
SellRecommendation
horizon별 최적 매도 시점 + 예상 가격 + 예상 손익
새로고침
사용자가 명시적으로 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 DB
manual_holdings 테이블 신설, JPA 영속화
기존 도메인 패턴과 일관성
—
프런트 LocalStorage
브라우저에만 저장
서버 불필요
디바이스 이동 불가, 데이터 유실 위험 → 미채택
방안 비교 — 신호 영속화 위치
방안
설명
왜 채택
미채택 대안
Backend MySQL 경유
Aggregator → Backend API → MySQL
기존 패턴 일관성, 단일 DB 소유 주체
—
Aggregator 직접 MySQL
Aggregator에 JPA 추가
네트워크 홉 감소
아키텍처 일관성 훼손 → 미채택
Redis TTL 대폭 확대
TTL을 24h로 늘림
코드 변경 최소
재시작 시 초기화, 영속성 없음, 근본 해결 아님 → 미채택
Detail Design
AS-IS / TO-BE 비교
항목
AS-IS
TO-BE
LLM 호출 시점
TTL 10분 만료 시 자동
사용자 새로고침 요청 시만
신호 캐시 레이어
ML in-memory + Redis (이중)
MySQL 단일 영속화
예측 horizon
D+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
DB 마이그레이션 먼저 적용 (manual_holdings, signal_snapshots, recommendation_snapshots)