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
6.3 KiB
BAT-054: Design
- Status: confirmed
- Batch: BAT-054
Architecture Summary
그날-시작 체크포인트에서 fetch된 BrokerCredentialPayload를 실제 KISBroker 인스턴스에 주입하는 경로를 완성한다.
설계 핵심 두 가지:
KISBroker.reinitialize(credential)메서드:app_key/app_secret/account_ref를 원자적으로 교체,base_url/timeout/allow_live/mock은 유지. L1 token 캐시 무효화 포함.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() 내에서:
- L1 in-memory token 무효화:
self._token = None,self._token_exp = datetime.min(UTC). - 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에서 판단