Files
web-page-backend/docs/superpowers/plans/2026-05-18-track-a-cache-hardening.md
gahusb 11d86450c3 docs(plan): Track A cache hardening (SP-A1 + SP-A2)
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>
2026-05-18 21:30:43 +09:00

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=True cache 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 pulldocker 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 작성 순서 권장"대로:

  1. Plan-B-Base — SP-1 (Redis) + SP-2 (WSL2)
  2. Plan-B-Insta — SP-3 + SP-4
  3. Plan-B-Music — SP-5 + SP-6
  4. Plan-B-Video — SP-7 + SP-8
  5. Plan-B-Infra — SP-9 + SP-10

각각은 별도 brainstorm 없이 spec §10에서 직접 plan 작성 가능 (이미 명세 충분).