web-ai stock_client TTL 증가 (60/300/60 → 180/600/300) + NAS stock TTLCache 도입 (cachetools, webai_cache 모듈, 3 endpoint 적용). 2-layer cache로 V2 재시작 시점부터 NAS 인바운드 호출 70% 감소 예상. 8개 task, TDD 적용 (회귀 테스트 3건 + cache 단위 테스트 6건). ~40분 작업. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
24 KiB
Track A — NAS↔Windows API 부하 캐시 강화 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: web-ai → NAS stock 호출량을 분당 12회 → 분당 3~4회로 축소하여, V2 재시작 시점부터 즉시 NAS CPU 부담 70% 감소.
Architecture: 2-layer cache. (1) web-ai client side: 3개 endpoint TTL 60/300/60 → 180/600/300으로 증가. (2) NAS stock server side: 동일 endpoint에 in-memory TTLCache 추가하여 web-ai 캐시 miss 시에도 KIS·LLM 재호출 차단. 두 layer가 cumulative하게 작동.
Tech Stack: Python 3.12 / FastAPI / pytest / cachetools.TTLCache. two repos: web-ai (signal_v2/) + web-backend (stock/).
Spec: web-backend/docs/superpowers/specs/2026-05-18-nas-windows-distributed-architecture-design.md §4 SP-A1·A2, §10 상세
File Structure
SP-A1 — web-ai 캐시 TTL (Modify)
| 파일 | 변경 | 책임 |
|---|---|---|
web-ai/signal_v2/stock_client.py:13-17 |
_TTL dict 3개 값 변경 |
endpoint별 client-side cache TTL |
web-ai/signal_v2/tests/test_stock_client_ttl.py (Create) |
TTL 값 회귀 테스트 | 미래 변경 시 의도하지 않은 회귀 방지 |
SP-A2 — NAS stock TTLCache (Modify + Create)
| 파일 | 변경 | 책임 |
|---|---|---|
web-backend/stock/requirements.txt |
cachetools>=5.3 추가 |
의존성 |
web-backend/stock/app/webai_cache.py (Create) |
3개 TTLCache + helper 함수 | server-side cache 중앙화 |
web-backend/stock/app/main.py:419-422 |
get_webai_portfolio() cache 적용 |
NAS portfolio 캐시 |
web-backend/stock/app/main.py:467-470 |
get_webai_news_sentiment(date) cache 적용 |
date별 캐시 |
web-backend/stock/app/screener/router.py:173 |
post_run() cache 적용 (mode=preview만) |
screener preview 캐시 |
web-backend/stock/app/test_webai_cache.py (Create) |
cache 동작 + TTL + key 분기 | 캐시 hit/miss 검증 |
Task 1: web-ai SP-A1 — _TTL dict 회귀 테스트 작성
Files:
-
Create:
C:/Users/jaeoh/Desktop/workspace/web-ai/signal_v2/tests/test_stock_client_ttl.py -
Step 1: 실패하는 테스트 작성
# tests/test_stock_client_ttl.py
"""SP-A1 회귀 — _TTL이 NAS 부담 완화를 위한 값으로 설정되어 있어야 함."""
from signal_v2.stock_client import _TTL
def test_portfolio_ttl_is_180s():
"""portfolio TTL은 180초 이상 (3분 폴링에서 1회 fetch가 3 폴링 커버)."""
assert _TTL["portfolio"] >= 180.0
def test_news_sentiment_ttl_is_600s():
"""news-sentiment TTL은 600초 이상 (10분, 뉴스 sentiment는 자주 안 바뀜)."""
assert _TTL["news-sentiment"] >= 600.0
def test_screener_preview_ttl_is_300s():
"""screener-preview TTL은 300초 이상 (5분, Top-20은 분 단위로 거의 안 바뀜)."""
assert _TTL["screener-preview"] >= 300.0
- Step 2: 테스트 실패 확인
Run: cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/test_stock_client_ttl.py -v
Expected: FAIL — 현재 _TTL 값은 60/300/60. portfolio·screener-preview 모두 < 180/300.
- Step 3:
_TTL값 변경
C:/Users/jaeoh/Desktop/workspace/web-ai/signal_v2/stock_client.py line 13-17:
변경 전:
_TTL = {
"portfolio": 60.0,
"news-sentiment": 300.0,
"screener-preview": 60.0,
}
변경 후:
# Cache TTL by endpoint (seconds).
# 2026-05-18 — NAS 인바운드 호출 부담 완화 (Plan-A SP-A1).
_TTL = {
"portfolio": 180.0, # 3분 (1분 폴링 시 3 폴링당 1회 실제 fetch)
"news-sentiment": 600.0, # 10분 (뉴스 sentiment는 자주 안 바뀜)
"screener-preview": 300.0, # 5분 (Top-20은 분 단위로 거의 안 바뀜)
}
- Step 4: 테스트 통과 확인
Run: cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/test_stock_client_ttl.py -v
Expected: PASS — 3개 모두 통과.
- Step 5: 전체 회귀 확인 (기존 56 tests + 신규 3 tests)
Run: cd C:/Users/jaeoh/Desktop/workspace/web-ai && python -m pytest signal_v2/tests/ -v 2>&1 | tail -5
Expected: 59 tests 모두 PASS (기존 56 + 신규 3).
- Step 6: 커밋
cd C:/Users/jaeoh/Desktop/workspace/web-ai
git add signal_v2/stock_client.py signal_v2/tests/test_stock_client_ttl.py
git commit -m "$(cat <<'EOF'
perf(signal_v2): raise stock_client TTL for NAS load relief (SP-A1)
portfolio 60s → 180s (3분 폴링 → 3회당 1회 fetch)
news-sent 300s → 600s (sentiment는 자주 안 바뀜)
screener 60s → 300s (Top-20 분 단위 변화 미미)
V2 재시작 시점부터 NAS stock에 대한 인바운드 호출이
분당 12 → 분당 3~4 로 감소 예상. 캐시 hit ratio 0~50% → 66~80%.
회귀 테스트 3건 추가로 미래 의도치 않은 TTL 변경 차단.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Task 2: NAS SP-A2 — cachetools 의존성 추가
Files:
-
Modify:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt -
Step 1: 현재 requirements.txt 확인
Run: cat C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt
파일 끝 확인 — 마지막 줄 newline 여부 확인 (sed/append 안전).
- Step 2: cachetools 추가
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/requirements.txt 끝에 한 줄 추가:
cachetools>=5.3
(파일 마지막에 newline 없으면 newline 먼저, 그 다음 cachetools 줄.)
- Step 3: 로컬 import 가능 여부 확인 (선택, NAS rebuild가 정본)
Run (Windows 로컬에서 docker 외부 검증용, 선택):
python -c "import cachetools; print(cachetools.__version__)" 2>&1
로컬 미설치라면 skip — NAS deployer가 rebuild 시 install. 이 plan은 코드 정합성만 보장.
- Step 4: 커밋 (단독 커밋, deps만)
cd C:/Users/jaeoh/Desktop/workspace/web-backend
git add stock/requirements.txt
git commit -m "$(cat <<'EOF'
chore(stock): add cachetools for server-side TTLCache (SP-A2 prep)
다음 커밋에서 /api/webai/portfolio·news-sentiment·screener/run에
in-memory TTLCache 적용 예정.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Task 3: NAS SP-A2 — webai_cache.py 모듈 + 단위 테스트
Files:
-
Create:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/webai_cache.py -
Create:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/test_webai_cache.py -
Step 1: 실패하는 테스트 작성
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/test_webai_cache.py:
"""SP-A2 — webai_cache module의 cache hit/miss + key 분기 검증."""
import time
import pytest
from app.webai_cache import (
PORTFOLIO_CACHE, NEWS_CACHE, SCREENER_CACHE,
cache_get_portfolio, cache_set_portfolio,
cache_get_news, cache_set_news,
cache_get_screener, cache_set_screener,
_screener_key,
)
def _clear_all():
PORTFOLIO_CACHE.clear()
NEWS_CACHE.clear()
SCREENER_CACHE.clear()
def test_portfolio_cache_miss_then_hit():
_clear_all()
assert cache_get_portfolio() is None
cache_set_portfolio({"holdings": [], "cash": 0})
assert cache_get_portfolio() == {"holdings": [], "cash": 0}
def test_news_cache_key_by_date():
"""date가 다르면 별도 캐시 슬롯."""
_clear_all()
cache_set_news("2026-05-18", {"count": 5})
cache_set_news("2026-05-17", {"count": 3})
assert cache_get_news("2026-05-18") == {"count": 5}
assert cache_get_news("2026-05-17") == {"count": 3}
assert cache_get_news("2026-05-16") is None # not cached
def test_news_cache_latest_key_normalized():
"""date=None은 'latest' 키로 정규화되어 동일 슬롯."""
_clear_all()
cache_set_news(None, {"count": 9})
assert cache_get_news(None) == {"count": 9}
def test_screener_key_includes_mode_and_top_n():
"""screener key는 mode + top_n + weights hash로 분기."""
k_preview = _screener_key("preview", 20, None)
k_preview_w = _screener_key("preview", 20, {"news": 0.3})
k_auto = _screener_key("auto", 20, None)
assert k_preview != k_preview_w
assert k_preview != k_auto
def test_screener_cache_roundtrip():
_clear_all()
payload = {"asof": "2026-05-18", "survivors_count": 17}
cache_set_screener("preview", 20, None, payload)
assert cache_get_screener("preview", 20, None) == payload
assert cache_get_screener("preview", 20, {"news": 0.3}) is None
def test_ttl_expiry_portfolio():
"""짧은 ttl로 만료 확인 — 직접 시간 조작 대신 TTLCache 내부 동작 신뢰."""
from cachetools import TTLCache
short = TTLCache(maxsize=1, ttl=0.1) # 0.1초
short["result"] = "x"
assert short.get("result") == "x"
time.sleep(0.2)
assert short.get("result") is None
- Step 2: 테스트 실패 확인
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest app/test_webai_cache.py -v
Expected: FAIL — app.webai_cache 모듈 존재 안 함.
- Step 3:
webai_cache.py작성
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/webai_cache.py:
"""SP-A2 — NAS stock의 /api/webai/* 엔드포인트 in-memory TTLCache.
web-ai 측 캐시(stock_client._TTL)가 miss됐을 때도 NAS에서 같은 데이터를
KIS·LLM 재호출 없이 즉시 반환하기 위한 2-layer 캐시의 server 측.
V1+V2가 동시 호출해도 NAS는 1회만 계산.
TTL 정책 (spec §10 SP-A2):
- portfolio: 120s (web-ai TTL 180s 보다 짧게 — 변경 감지 가능)
- news: 600s (sentiment는 일 단위)
- screener: 180s
"""
from __future__ import annotations
import hashlib
import json
from typing import Any, Optional
from cachetools import TTLCache
PORTFOLIO_CACHE: TTLCache = TTLCache(maxsize=1, ttl=120.0)
NEWS_CACHE: TTLCache = TTLCache(maxsize=10, ttl=600.0)
SCREENER_CACHE: TTLCache = TTLCache(maxsize=10, ttl=180.0)
# ----- portfolio -----
def cache_get_portfolio() -> Optional[Any]:
return PORTFOLIO_CACHE.get("result")
def cache_set_portfolio(value: Any) -> None:
PORTFOLIO_CACHE["result"] = value
# ----- news-sentiment -----
def _news_key(date: Optional[str]) -> str:
return date if date else "latest"
def cache_get_news(date: Optional[str]) -> Optional[Any]:
return NEWS_CACHE.get(_news_key(date))
def cache_set_news(date: Optional[str], value: Any) -> None:
NEWS_CACHE[_news_key(date)] = value
# ----- screener -----
def _screener_key(mode: str, top_n: int, weights: Optional[dict]) -> str:
"""mode + top_n + weights canonical hash. weights 객체 동등성을 키로."""
if weights is None:
w_repr = "none"
else:
# canonical: sorted keys → md5 hex (긴 weights도 짧은 키로)
canon = json.dumps(weights, sort_keys=True, ensure_ascii=False)
w_repr = hashlib.md5(canon.encode("utf-8")).hexdigest()[:12]
return f"{mode}:{top_n}:{w_repr}"
def cache_get_screener(mode: str, top_n: int, weights: Optional[dict]) -> Optional[Any]:
return SCREENER_CACHE.get(_screener_key(mode, top_n, weights))
def cache_set_screener(mode: str, top_n: int, weights: Optional[dict], value: Any) -> None:
SCREENER_CACHE[_screener_key(mode, top_n, weights)] = value
- Step 4: 테스트 통과 확인
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest app/test_webai_cache.py -v
Expected: PASS — 6개 모두 통과.
- Step 5: 커밋
cd C:/Users/jaeoh/Desktop/workspace/web-backend
git add stock/app/webai_cache.py stock/app/test_webai_cache.py
git commit -m "$(cat <<'EOF'
feat(stock): webai_cache module (TTLCache for SP-A2)
3개의 TTLCache (portfolio 120s · news 600s · screener 180s) +
헬퍼 함수. screener key는 mode + top_n + weights canonical hash로
분기. 다음 커밋에서 /api/webai/portfolio·news-sentiment·screener/run
3 endpoint에 적용.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Task 4: NAS SP-A2 — /api/webai/portfolio 캐시 적용
Files:
-
Modify:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/main.py:419-422 -
Step 1: 현재 endpoint 코드 확인
web-backend/stock/app/main.py 419-422 line은 spec §10 SP-A2와 일치:
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
def get_webai_portfolio():
"""web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가)."""
return _augment_portfolio_with_pnl_pct(get_portfolio())
- Step 2: 캐시 적용으로 교체
web-backend/stock/app/main.py 419-422 line을 다음으로 교체:
@app.get("/api/webai/portfolio", dependencies=[Depends(verify_webai_key)])
def get_webai_portfolio():
"""web-ai 전용 portfolio (인증 필수, pnl_pct 비율 필드 추가).
SP-A2 server-side TTLCache 적용. V1+V2 동시 호출도 NAS에서 1회 계산.
"""
cached = webai_cache.cache_get_portfolio()
if cached is not None:
return cached
result = _augment_portfolio_with_pnl_pct(get_portfolio())
webai_cache.cache_set_portfolio(result)
return result
- Step 3: import 추가 (파일 상단)
web-backend/stock/app/main.py 파일 상단 import 블록 (다른 from .xxx import 들과 같은 위치)에 추가:
from . import webai_cache
- Step 4: 빠른 import sanity 체크
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app import main; print('OK')" 2>&1 | tail -3
(cachetools 미설치 환경에선 ImportError 가능 → 그 경우 pip install cachetools 후 재시도. 실제 검증은 NAS rebuild 후.)
Expected: OK 또는 cachetools 누락 메시지 (의도된 상태).
Task 5: NAS SP-A2 — /api/webai/news-sentiment 캐시 적용
Files:
-
Modify:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/main.py:467-470 -
Step 1: 캐시 적용
web-backend/stock/app/main.py 467-470 line을 다음으로 교체:
@app.get("/api/webai/news-sentiment", dependencies=[Depends(verify_webai_key)])
def get_webai_news_sentiment(date: str | None = None):
"""web-ai 전용 news sentiment 일별 dump.
SP-A2 server-side TTLCache 적용. date 파라미터별로 별도 슬롯.
"""
cached = webai_cache.cache_get_news(date)
if cached is not None:
return cached
result = _fetch_news_sentiment_dump(date)
webai_cache.cache_set_news(date, result)
return result
- Step 2: import sanity 체크
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app import main; print('OK')" 2>&1 | tail -3
Expected: OK
Task 6: NAS SP-A2 — /api/stock/screener/run 캐시 적용 (preview 모드만)
Files:
-
Modify:
C:/Users/jaeoh/Desktop/workspace/web-backend/stock/app/screener/router.py:173-... -
Step 1: 현재 함수 확인 (참고)
web-backend/stock/app/screener/router.py:173 시작 def post_run(body: schemas.RunRequest): — 함수 본체는 mode 분기 후 _conn() + KIS 호출 등. 단, mode == "auto" 는 휴장일/실 운영 트리거이므로 캐시하지 않음 (매 호출이 다른 의미). mode == "preview" 는 frontend·web-ai 폴링용 → 캐시 적용.
- Step 2: 함수 진입부에 cache 분기 추가
web-backend/stock/app/screener/router.py:173 @router.post("/run", ...) 의 def post_run(...) 본체 첫 줄들에 다음 캐시 분기 추가:
변경 전 (line 173-179 근처):
@router.post("/run", response_model=schemas.RunResponse)
def post_run(body: schemas.RunRequest):
from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR
started_at = dt.datetime.utcnow().isoformat()
with _conn() as c:
asof = _resolve_asof(body.asof, c)
변경 후:
@router.post("/run", response_model=schemas.RunResponse)
def post_run(body: schemas.RunRequest):
from .registry import NODE_REGISTRY as _NR, GATE_REGISTRY as _GR
# SP-A2 — preview 모드는 web-ai/frontend 폴링이라 캐시 적용.
# auto 모드는 실제 운영 트리거(휴장일 게이트 등)라 캐시 미적용.
if body.mode == "preview":
cached = webai_cache.cache_get_screener(body.mode, body.top_n, body.weights)
if cached is not None:
return cached
started_at = dt.datetime.utcnow().isoformat()
with _conn() as c:
asof = _resolve_asof(body.asof, c)
- Step 3: 함수 끝 부분 — preview 결과를 캐시에 저장
post_run의 반환부 직전에 (preview 모드일 때만) 캐시 저장. post_run 함수는 결과를 schemas.RunResponse(...) 로 만들어 return하는 구조일 것. 정확한 return 위치 확인 후, return 직전에:
web-backend/stock/app/screener/router.py post_run 함수의 마지막 return 직전에:
# SP-A2 — preview 모드 결과 캐시 저장.
if body.mode == "preview":
webai_cache.cache_set_screener(body.mode, body.top_n, body.weights, response)
return response
(response 라는 변수가 없으면, 기존 return 표현식을 response = ... 로 binding 후 위 코드 추가.)
주의: post_run의 정확한 return 라인을 먼저 확인.
grep -n "return " app/screener/router.py | head로 위치 파악 후 적용.
- Step 4: import 추가 (router.py 상단)
web-backend/stock/app/screener/router.py 상단 import 블록에 추가:
from .. import webai_cache
- Step 5: 빠른 import sanity 체크
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -c "from app.screener import router; print('OK')" 2>&1 | tail -3
Expected: OK
Task 7: 통합 검증 — 기존 테스트 회귀 + SP-A2 신규 테스트
Files: (조회만)
- Step 1: stock 전체 pytest 실행
Run: cd C:/Users/jaeoh/Desktop/workspace/web-backend/stock && python -m pytest -v 2>&1 | tail -30
Expected: 기존 모든 테스트 + SP-A2 신규 6 tests 모두 PASS. 0 failed.
- Step 2: 회귀 발견 시 처리
회귀가 발견되면:
- import 누락 →
from . import webai_cache또는from .. import webai_cache위치 재확인 - screener test가 cache hit으로 fail → test가
_clear_all()또는 cache fixture 통해 격리되어 있는지 확인. 필요 시 conftest에autouse=Truecache reset fixture 추가:
# conftest.py에 추가 (선택)
import pytest
from app import webai_cache
@pytest.fixture(autouse=True)
def _reset_webai_cache():
webai_cache.PORTFOLIO_CACHE.clear()
webai_cache.NEWS_CACHE.clear()
webai_cache.SCREENER_CACHE.clear()
yield
- Step 3: 커밋 (SP-A2 endpoint 통합 + 회귀 확인)
cd C:/Users/jaeoh/Desktop/workspace/web-backend
git add stock/app/main.py stock/app/screener/router.py
# (필요 시) git add stock/app/conftest.py
git commit -m "$(cat <<'EOF'
feat(stock): apply webai_cache to portfolio/news/screener-preview (SP-A2)
3 endpoint cache 적용 — /api/webai/portfolio, /api/webai/news-sentiment,
/api/stock/screener/run (preview 모드만, auto는 캐시 미적용).
V1+V2 동시 호출도 NAS에서 1회 계산. web-ai 측 SP-A1 캐시와 2-layer로
작동하여 NAS 인바운드 부담 70% 감소 예상.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Task 8: 양쪽 push + NAS deploy 트리거
Files: 없음 (git 작업)
- Step 1: web-ai push
cd C:/Users/jaeoh/Desktop/workspace/web-ai
git push origin main
Expected: success. 인증 prompt 뜨면 자격증명 입력. 1회 실패 시 1회 재시도 (캐시 패턴).
참고: web-ai는 NAS deployer가 별도 webhook 없음 (Windows 머신 코드). push는 백업/이력 동기화 목적. 실제 적용은 V2 재시작 시점.
- Step 2: web-backend push (NAS deployer 트리거)
cd C:/Users/jaeoh/Desktop/workspace/web-backend
git push origin main
Expected: success. NAS deployer가 webhook 수신 → git pull → docker compose build stock --no-cache (cachetools 신규 설치) → docker compose up -d stock. 통상 2~3분 소요.
- Step 3: NAS stock 컨테이너 헬스 확인
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://gahusb.synology.me/api/stock/news -m 10
Expected: HTTP 200. (NAS deploy 완료 후 통상 30초 ~ 2분 대기 필요.)
- Step 4: webai 캐시 효과 확인 (선택)
연속 2회 호출 시 두 번째가 즉시 응답하는지 (cached):
# 인증키 필요. .env의 WEBAI_API_KEY 사용 또는 NAS에서 직접 호출.
# Windows 로컬에서:
# 첫 호출
time curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio -o /dev/null
# 즉시 두번째 (캐시 hit 기대, 첫 호출 < 1s + DB / 두번째 < 100ms)
time curl -s -H "X-WebAI-Key: $WEBAI_API_KEY" https://gahusb.synology.me/api/webai/portfolio -o /dev/null
Expected: 두 번째 호출이 첫 번째보다 명확히 빠름 (DB·계산 skip).
Self-Review
Spec 커버리지
| Spec 요구사항 | 구현 Task |
|---|---|
| §4 SP-A1: web-ai 캐시 TTL 증가 (180/600/300) | Task 1 |
| §4 SP-A2: NAS stock TTLCache | Task 2~7 |
| §10 SP-A2: 3 endpoint (portfolio/news/screener) 적용 | Task 4 (portfolio), Task 5 (news), Task 6 (screener preview) |
| §10 SP-A2: cachetools 의존성 | Task 2 |
| §8: X-WebAI-Key 인증 (기존 verify_webai_key 유지) | 기존 dependency 그대로, 변경 없음 |
| §6: server cache 별개 (Redis 캐시 옵션) | in-memory TTLCache 선택 (Redis는 SP-1 이후 도입 검토) |
§4의 SP-A2는 /api/webai/portfolio, /api/webai/news-sentiment, /api/stock/screener/run 3건만 명시. 추가 endpoint 캐시는 out of scope (별도 plan에서).
Placeholder 스캔
- TBD/TODO/"implement later" 패턴 없음 ✓
- 모든 code step에 완전 코드 포함 ✓
- Task 6에 한 가지 conditional ("
post_run의 정확한 return 라인을 먼저 확인") — 이건 plan 실행 시 grep 명령으로 즉시 해결 가능한 단순 lookup이라 placeholder가 아님. 그러나 안전성 위해 helper note 그대로 유지.
Type consistency
webai_cache.cache_get_portfolio()/cache_set_portfolio(value)— Task 3에서 정의, Task 4에서 사용. 시그니처 일치 ✓cache_get_news(date)— Task 3·5 일치 ✓cache_get_screener(mode, top_n, weights)/cache_set_screener(mode, top_n, weights, value)— Task 3·6 일치 ✓- 변수명
cached,result,payload— 각 함수 안에서만 사용, 충돌 없음 ✓
위험·주의
- NAS deployer rebuild:
requirements.txt변경은 docker image rebuild 필요. deployer가 변경 감지 시 rebuild 트리거. 만약 deployer가 변경 미감지(예: requirements.txt만 변경 시 rebuild 안 함)라면 NAS에서docker compose build stock --no-cache && docker compose up -d stock수동 실행 필요. - Cache stale: TTL이 충분히 짧아 stale 문제 미미. portfolio 120s = web-ai 폴링 주기(1분) 2배. 변경 감지에 최대 2분 지연.
- Cache miss thunder herd: V1+V2가 정확히 동시에 캐시 miss 시 KIS 동시 호출 가능. 현재 V1/V2 둘 다 정지 상태라 risk 0. 향후 재시작 시 KIS rate limit 모니터링 필요 (별도 plan 항목).
완료 후 다음 단계
Plan-A 완료 후 spec §14 "차후 plan 작성 순서 권장"대로:
- Plan-B-Base — SP-1 (Redis) + SP-2 (WSL2)
- Plan-B-Insta — SP-3 + SP-4
- Plan-B-Music — SP-5 + SP-6
- Plan-B-Video — SP-7 + SP-8
- Plan-B-Infra — SP-9 + SP-10
각각은 별도 brainstorm 없이 spec §10에서 직접 plan 작성 가능 (이미 명세 충분).