Files
trex-core/docs/batches/bat-054_20260628_daystart-credential-kis-inject/design.md
T
genichinandClaude Sonnet 4.6 6b066e19cb feat(bat-054): 그날-시작 KIS 인증정보 양의 경로 주입 — KISBroker reinitialize (REQ-113 AC-2/AC-4, REQ-115 AC-1)
BAT-053 스캐폴드에서 연기된 양의 경로 완성:
- KISBroker.reinitialize(credential): app_key/app_secret/account_ref 교체 + L1 token 무효화
- SnapshotScheduler에 kis_reinit_callback 주입 지점 추가; 그날-시작 성공 시 callback 호출
- daemon.py에 _build_kis_reinit_callback() 헬퍼 + SnapshotScheduler 조립 수정
- 신규 테스트 12개 추가: 757 passed / 47 skipped (기존 745 대비 +12, 회귀 0)

안전 불변식 유지:
- credential_fetch_enabled=False(기본) 경로 byte-for-byte 보존
- fail-closed 불변식 미파괴 (reinit 실패 시 entry_allowed 유지)
- AMQP wire 계약 변경 없음

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
2026-06-28 19:51:48 +09:00

6.3 KiB

BAT-054: Design

  • Status: confirmed
  • Batch: BAT-054

Architecture Summary

그날-시작 체크포인트에서 fetch된 BrokerCredentialPayload를 실제 KISBroker 인스턴스에 주입하는 경로를 완성한다.

설계 핵심 두 가지:

  1. KISBroker.reinitialize(credential) 메서드: app_key/app_secret/account_ref를 원자적으로 교체, base_url/timeout/allow_live/mock은 유지. L1 token 캐시 무효화 포함.
  2. SnapshotScheduler에 kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None 추가. 그날-시작 성공 시 callback 호출 → KISBroker.reinitialize 트리거.

Changed Areas

  • Docs: docs/batches/bat-054_.../ (이번 batch 문서)
  • Modules:
    • trex_core/adapters/broker/kis.py — reinitialize() 메서드 추가
    • trex_core/application/snapshot_scheduler.py — kis_reinit_callback 인자 추가 + 성공 시 호출
    • trex_core/runtime/daemon.py — _build_kis_reinit_callback() + SnapshotScheduler 조립 수정
  • Interfaces: 없음 (AMQP wire 계약 변경 없음)

Key Decisions

D1: KISBroker.reinitialize() — 인스턴스 변수 직접 교체

결정: KISBroker에 reinitialize(credential: BrokerCredentialPayload) -> None 메서드를 추가한다. 메서드 내에서:

  • self._app_key, self._app_secret, self._account_ref를 새 값으로 교체
  • L1 token 캐시(self._token = None, self._token_exp = datetime.min) 무효화
  • L2 token cache(Redis, token_cache.invalidate가 있으면 호출; 없으면 skip — fallback graceful)
  • base_url/timeout/mock/allow_live는 불변 (env에서 온 인프라 설정)

근거:

  • KISBroker는 이미 실행 중인 여러 스레드(execution loop, safety-sell)가 공유하는 인스턴스다. 새 객체로 교체하면 기존 참조가 stale해진다.
  • 인스턴스 변수 교체는 CPython GIL 하에서 atomic하며, 진행 중인 HTTP 요청이 완료된 후 다음 _ensure_token() 호출부터 새 자격증명을 사용한다. 그날-시작(07:30 KST)은 장 시작 전이므로 경쟁 조건 위험이 낮다.
  • threading.Lock은 추가하지 않는다(reinitialize가 그날-시작 1회만 호출되고 장 시작 전이라 안전). 만약 향후 장중 재초기화가 필요해지면 그때 lock을 추가한다(REQ-115 설계: 그날 고정 캐시로 장중 재fetch 없음).

D2: SnapshotScheduler.kis_reinit_callback — Optional callable 주입

결정: SnapshotScheduler.__init__에 kis_reinit_callback: Optional[Callable[[BrokerCredentialPayload], None]] = None 추가. _maybe_fetch_credential() 성공 후, 캐시 저장 + INFO 로그 이후에 callback을 호출한다.

# 성공 경로
self._cached_credential = ctx.broker
self._credential_date = today
self._entry_allowed = True
_log.info("DAY_START credential loaded: account_ref=%s ...", ...)
if self._kis_reinit_callback is not None:
    try:
        self._kis_reinit_callback(ctx.broker)
        _log.info("DAY_START KIS broker reinitialized with fetched credential (instance=%s)", ...)
    except Exception as exc:
        _log.error("DAY_START KIS broker reinitialize failed: %s (instance=%s) — entry remains allowed", ...)
        # 재초기화 실패는 entry_allowed를 False로 전환하지 않는다(기존 env 인증정보로 계속 운행)

근거:

  • callback 실패가 entry_allowed를 뒤집지 않는다: 재초기화 실패 시 기존 env 인증정보로 운행 계속 (fail-safe: 이미 기동 중인 브로커가 있으므로).
  • 새 인증정보 주입에 성공하면 INFO 로그로 가시화.
  • credential_fetch_enabled=False(기본)일 때 callback이 None이므로 코드 경로 완전 미진입 → 기존 동작 byte-for-byte 보존.

D3: daemon.py — callback 조립

결정: daemon.py에 private helper _build_kis_reinit_callback(config, broker) 추가:

def _build_kis_reinit_callback(config, broker):
    if config.broker != "kis" or not config.credential_fetch_enabled:
        return None
    if not hasattr(broker, "reinitialize"):
        return None
    return broker.reinitialize

이를 SnapshotScheduler 생성 시 kis_reinit_callback=_build_kis_reinit_callback(config, self.broker) 로 전달.

근거:

  • broker == 'paper'이면 callback=None → 기존 경로 보존.
  • credential_fetch_enabled=False이면 callback=None.
  • KISBroker가 아닌 브로커가 reinitialize를 가지지 않을 경우 graceful skip.

D4: access token cache (Redis, REQ-016) 무효화

결정: reinitialize() 내에서:

  1. L1 in-memory token 무효화: self._token = None, self._token_exp = datetime.min(UTC).
  2. L2 Redis: token_cache에 invalidate(account_ref, app_key) 메서드가 있으면 호출, 없으면 skip. RedisTokenCache는 기존 key scheme이 (account_ref, app_key) 기반이므로 기존 key가 자연스럽게 miss로 처리된다(새 account_ref/app_key로 조회 시 miss → 새 토큰 발급). 명시적 invalidate API는 추가하지 않는다.

근거: 새 account_ref/app_key로 L2 조회 시 miss → 새 토큰 발급. 구 key는 TTL로 자연 만료. 인증정보 변경이 1일 1회이므로 수동 무효화 복잡성 불필요.

D5: REQ-113 AC-4 보강 — break-glass 로그 assert

결정: tests/application/test_snapshot_scheduler.py에 break-glass 활성화 시 로그에 "break-glass" 키워드가 포함됨을 caplog로 검증하는 케이스 추가.

Edge Cases

  • credential_fetch_enabled=True이고 broker='paper'인 경우: callback=None이므로 재초기화 없음. entry_allowed는 fetch 결과에 따라 설정됨(논리적으로 paper broker에 fetch가 의미 없으나 테스트 가능).
  • reinitialize() 호출 중 in-flight HTTP 요청: CPython GIL 하에서 변수 할당은 atomic, 다음 _ensure_token() 호출부터 새 값 사용. 경쟁 조건 없음(그날-시작 = 장 전).
  • credential_fetch_enabled=True이고 break-glass: callback 호출 없음(재초기화 없이 env 인증정보 사용) — D2 기술.

Architecture Impact

  • runtime-flows: KISBroker 재초기화 경로 추가 (경미한 갱신)
  • project-structure: 변경 없음

Reference Doc Update Plan

  • project-structure.md: none (구조 변경 없음)
  • runtime-flows.md: DAY_START 흐름에 KISBroker reinitialize 스텝 추가 설명이 유용하나 이번 batch 필수 아님 — verification에서 판단