# 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을 호출한다. ```python # 성공 경로 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)` 추가: ```python 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에서 판단