# finestock 개선 제안 코드베이스 전반(`finestock/`, `tests/`, 루트 예제 스크립트, 패키징 파일)을 훑어보고 발견한 문제점과 개선 방향을 정리했다. 심각도가 높은 순으로 배치했다. ## 0. 아키텍처 버그/코드 스멜과는 별개로, 설계 자체를 평가하면 다음과 같다. ### 잘 된 부분 - **Facade + Factory**: `create_api(APIProvider.X)`로 브로커 하나를 받아 인증/시세/주문/실시간을 한 객체에서 다루게 한 것은 "브로커를 갈아끼워도 호출부는 그대로"라는 목표에 잘 맞는 선택이다. `APIFactory`가 브로커별 모듈을 lazy-import하는 것도 미사용 브로커의 의존성을 끌고 오지 않는 합리적인 설계다. - **`path.py`로 도메인/엔드포인트를 데이터화**: URL 문자열이 코드 곳곳에 흩어지지 않고 `_API_PATH_[클래스명]` 한 곳에 모여 있다. `EBest(LS)`, `LSV(LS)`, `KiwoomV(Kiwoom)`처럼 실전↔모의 차이가 "도메인만 다름"인 브로커는 실제로 상속만으로 몇 줄짜리 서브클래스가 된다(`ebest.py`, `ls_v.py`, `kiwoom_v.py` 확인 완료). **이 축(도메인 차이)에 대해서는 설계 의도대로 잘 작동한다.** - **Queue 주입 방식의 실시간 데이터 흐름**: 콜백 지옥 대신 `set_data_queue()`로 소비자가 원하는 큐를 주입받는 구조라, WS 루프와 소비자 로직이 느슨하게 결합된다. asyncio 루프 안에 비즈니스 로직을 얽어 넣지 않아도 되는 점은 장점이다. - **frozen dataclass로 정규화된 모델**: 브로커마다 제각각인 JSON 응답을 `Price`/`Order`/`Trade` 등 불변 객체로 통일해서 반환하는 규약 자체는 일관되게 지켜지고 있다(`Kiwoom.do_order`/`do_order_cancel`만 이 규약을 깨고 raw dict를 반환하는 예외 — 5장 참고). ### 걸리는 부분 **0.1 ISP가 타입 힌트 수준에서만 존재하고, 실제로는 거꾸로 작동한다.** `BaseProvider`가 6개 인터페이스를 전부 합친 하나의 거대 클래스라서, 브로커가 지원하지 않는 기능도 무조건 구현해야 한다. 그 결과가 `Kis.get_index_list`의 `print("Kis not supported")`, `Kis.recv_price`/`recv_index`/... 의 빈 `pass`들이다. ISP의 원래 취지는 "클라이언트가 쓰지 않는 메서드에 의존하지 않게 하자"인데, 여기서는 "구현체가 쓸 수 없는 메서드도 강제로 구현하게" 만드는 쪽으로 뒤집혀 있다. 게다가 타입 힌트로 좁히는 것(`market_api: MarketDataProvider = full_api`)은 IDE 자동완성용일 뿐 런타임 강제력이 없어서, `TradingProvider`로 좁혀 받은 코드가 실수로 `do_order`를 호출하는 걸 막아주지 않는다. mypy 같은 정적 타입 검사가 파이프라인에 없어 이 이점조차 실제로는 활용되지 못하고 있다. **0.2 상속 축이 하나뿐이라, 두 번째 변형 축(TR ID 차이)이 나오자 코드 중복으로 샌다.** `path.py` 기반 상속은 "도메인만 다름" 케이스에는 잘 맞지만, KIS는 실전/모의가 **도메인도 다르고 TR ID도 다르다**. TR ID는 `path.py`처럼 데이터화되어 있지 않고 각 메서드 안에 문자열 리터럴로 박혀 있다(`header["tr_id"] = "TTTC8434R"`). 그래서 `KisV`는 `get_balance`, `do_order`를 새로 쓰는 대신 **거의 통째로 복붙**해서 TR ID 한 줄만 바꾼 형태가 됐다(`kis_v.py`). 이는 상속 설계 자체보다 "브로커별로 달라지는 축이 URL 하나가 아니라는 것"을 처음부터 모델링하지 않은 결과다. TR ID/설정값도 `path.py`처럼 별도 딕셔너리(`_TR_IDS_["Kis"]` vs `_TR_IDS_["KisV"]`)로 빼서 메서드 본문은 공유하고 설정만 주입하는 방식이었다면 `KisV`도 지금의 `EBest`/`LSV`만큼 얇아졌을 것이다. 이건 4장에 적은 코드 중복의 근본 원인이기도 해서, 아키텍처 레벨에서 고치면 4장 항목이 상당 부분 같이 해결된다. **0.3 HTTP/WS 클라이언트가 각 메서드에 직접 박혀 있어 교체·테스트·횡단 관심사 주입이 불가능하다.** `requests.post(...)`가 브로커당 10~15곳에 그대로 호출된다. 재시도, 레이트리밋, 타임아웃 정책, 목(mock) 대체를 넣으려면 그 10~15곳을 전부 고쳐야 한다는 뜻이다. 트랜스포트 계층(`self._client.post(...)` 같은 얇은 어댑터)을 한 겹 두는 것만으로 테스트 가능성과 횡단 관심사 적용이 동시에 해결되는데, 지금은 그 경계가 아예 없다(6장 테스트 커버리지 부재의 근본 원인이기도 하다). **0.4 `_init_path()`가 동적 `setattr`로 인스턴스 속성을 만들어서, "IDE 자동완성"이라는 설계 목표를 스스로 깎아먹는다.** `API._init_path()`는 딕셔너리를 순회하며 `self.DOMAIN`, `self.CHART` 등을 런타임에 만든다(`api.py:29-32`). README가 내세우는 설계 목표 중 하나가 "타입 힌팅으로 IDE 자동완성이 되게 한다"인데, 정작 `self.DOMAIN`/`self.CHART` 같은 핵심 속성은 정적으로 선언되어 있지 않아 IDE/mypy가 이 속성들의 존재를 모른다. `ClassVar`가 선언된 설정 dataclass(브로커별 서브클래스)로 바꾸면 중앙화 이점은 유지하면서 정적 분석 이점도 되찾을 수 있다. **0.5 동기(REST)와 비동기(WS)가 한 객체에 공존하는데 그 경계에 대한 설계가 없다.** 같은 파사드 객체가 블로킹 `requests` 호출 메서드와 `async def run()`/`recv_price` 같은 코루틴 메서드를 동시에 갖고 있다. asyncio 이벤트 루프를 돌리면서(`run()`) 그 안에서 블로킹 `get_ohlcv`를 호출하면 루프 전체가 멈춘다. 브로커 API 자체가 REST+WS 혼합이라 완전히 피하긴 어렵지만, 최소한 "REST 메서드는 블로킹이니 `asyncio.to_thread`로 감싸서 써라" 같은 경계 가이드나 헬퍼가 전혀 없다. **0.6 실시간 연결의 회복탄력성이 설계에 아예 없다.** `LS.run()`/`Kiwoom.run()`은 예외가 나면 그냥 `is_run = False`로 루프를 끝내버린다(재연결, 백오프, 재구독 없음). 실거래 봇의 실시간 클라이언트에서 이건 버그라기보다 "애초에 그 관심사를 다루는 레이어가 설계에 없다"는 문제라, 지금 구조 위에 패치를 얹기보다 재연결/재구독을 담당하는 별도 계층을 설계 단계에서 넣는 게 낫다. ### 아키텍처 개선 우선순위 Facade+Factory+ISP라는 큰 뼈대 자체는 목적에 맞고, "도메인 URL이 다른" 축까지는 실제로 깔끔하게 작동한다. 문제는 그 뼈대가 **한 개의 변형 축(도메인)만 상정**하고 있어서, TR ID 차이·기능 미지원·동기/비동기 혼재·트랜스포트 계층 부재 같은 나머지 축들이 전부 상속 복붙이나 빈 스텁으로 새어나가고 있다는 점이다. 다음 순서로 손대면 4~6장의 개별 항목 상당수가 부수적으로 정리된다. 1. TR ID/설정을 `path.py`처럼 데이터화해서 `KisV`류의 메서드 복붙 제거 2. 트랜스포트 계층(공통 요청 헬퍼/어댑터) 한 겹 추가 — 3장의 에러 처리 개선과 통합 3. `BaseProvider`를 강제 합성 대신, 브로커가 실제 지원하는 인터페이스만 mix-in 하는 구조로 전환 ## 1. 보안 (긴급) ### 1.1 실제 API 키/토큰이 저장소에 평문으로 존재 `example.py`, `example_async.py`에 실제로 보이는 `app_key`, `app_secret`, 계좌번호, JWT 형식의 `access_token`이 하드코딩되어 있다. ```python # example.py app_key = "PS6HIMzSNcpfSU29Qkdlr17szGoZmPMZ3kOC" app_secret = "fkaR4TJD3SU2MrbjIHHWbCufxIPcMAs1" account_num = "207087079" access_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9..." ``` **문제**: 현재 `.gitignore`는 JetBrains 관련 항목만 포함하고 있어 `example.py`, `debug.log`, `venv/`, `build/`, `dist/`, `__pycache__/`, `finestock.egg-info/`를 걸러주지 않는다(`git status`에 전부 `??`로 나타남). 즉 무심코 `git add .` 한 번이면 실키가 원격 저장소에 올라간다. 이미 커밋되지 않았는지도 확인이 필요하다. **조치**: - 즉시 위 키/토큰을 브로커사 콘솔에서 폐기(재발급)한다. - `.gitignore`에 `debug.log`, `__pycache__/`, `build/`, `dist/`, `*.egg-info/`, `venv/`를 추가한다. - 예제 파일의 실키를 `os.environ["LS_APP_KEY"]` 같은 환경변수 참조 또는 `YOUR_APP_KEY` 플레이스홀더로 교체한다. - 저장소 히스토리에 이미 커밋된 적이 있다면 `git filter-repo`/BFG로 히스토리에서 제거하는 것도 검토한다. ### 1.2 자격증명이 디버그 로그에 그대로 남는다 `API.oauth()`(`finestock/comm/api.py:67`), `LS`의 거의 모든 TR 메서드, `Kis.get_ohlcv` 등에서 요청 헤더/바디를 통째로 `logger.debug`로 남긴다. 헤더에는 `appkey`, `appsecret`, `authorization: Bearer `이 포함되어 있어 `debug.log`가 곧 자격증명 덤프가 된다. `debug.log`가 `.gitignore`에도 없어 실수로 커밋될 위험까지 겹친다(1.1 참고). **조치**: 로깅 전에 민감 필드를 마스킹하는 헬퍼(`_redact(headers)`)를 만들어 `appkey`, `appsecret`, `authorization` 값을 `***`로 치환한 뒤 로깅한다. ## 2. 실제 동작 버그 ### 2.1 `Kis.do_order`의 매도 TR ID에 트레일링 공백 ```python # finestock/kis/kis.py:152 header["tr_id"] = "TTTC0802U" if buy_flag == finestock.ORDER_FLAG.BUY else "TTTC0801U " # ← 끝에 공백 ``` 매도 주문 시 TR ID가 `"TTTC0801U "`로 전송되어 KIS 서버가 이를 유효하지 않은 TR로 거부할 가능성이 높다. 실거래 주문 코드의 오타이므로 우선순위가 높다. ### 2.2 KIS 조회 메서드의 기본 인자가 import 시점에 고정됨 ```python # finestock/kis/kis.py:47 def get_ohlcv(self, code, frdate=datetime.now().strftime('%Y%m%d'), todate=datetime.now().strftime('%Y%m%d')): ``` `get_index`도 동일 패턴(`kis.py:70`). 파이썬은 함수 정의 시점에 기본값을 한 번만 평가하므로, 모듈을 import한 날짜가 그대로 굳어버린다. 프로세스를 며칠 이상 띄워두는 트레이딩 봇에서 인자를 생략하고 호출하면 실제로는 "오늘"이 아니라 "프로세스 시작일"의 시세를 조회하게 된다. **조치**: `frdate=None`으로 받고 함수 본문에서 `frdate = frdate or datetime.now().strftime('%Y%m%d')`로 매 호출마다 평가한다. ### 2.3 `Kiwoom.get_ohlcv_min`에 중복 `return` ```python # finestock/kiwoom/kiwoom.py:127-129 return self._get_chart_sync("ka10080", params, next_key=tr_cont_key, frdate=cts_date if cts_date.strip() else "") return self._get_chart_sync("ka10080", params, next_key=tr_cont_key, frdate=cts_date if cts_date.strip() else "") ``` 두 번째 줄은 도달 불가능한 죽은 코드. 복붙 실수로 보이며 삭제 대상. ### 2.4 `LS.get_index_list`가 항상 `None` 반환 ```python # finestock/ls/ls.py:282-299 def get_index_list(self): ... response = requests.post(...) res = response.json() logger.debug(...) # ← 여기서 끝. res를 파싱해서 return하는 코드가 없음 ``` 호출부는 항상 `None`을 받는다. 인터페이스(`InfoProvider.get_index_list`)의 계약을 지키지 못하고 있어 구현이 비어있는 상태에 가깝다. ### 2.5 `path.py`의 `_KIWOOM_V_`에서 `DOMAIN` 키를 두 번 정의 ```python # finestock/path.py:66-76 _KIWOOM_V_ = { **_KIWOOM_, "DOMAIN": "https://api.kiwoom.com", # ← 바로 아래서 덮어써짐, 의미 없는 죽은 코드 ... "DOMAIN": "https://mockapi.kiwoom.com", "DOMAIN_WS": "wss://mockapi.kiwoom.com:10000/api/dostk/websocket", } ``` 동작 결과는 두 번째 값(`mockapi.kiwoom.com`)으로 정상 동작하지만, 첫 값과 그 위의 탐색용 주석("Wait, main DOMAIN is..." 등)이 그대로 남아있어 다음에 코드를 읽는 사람이 혼란스럽다. 결론만 남기고 정리 필요. ### 2.6 `LSV`(모의투자)가 `LS.oauth()`의 `scope: "oob"`를 그대로 상속 `finestock/ls/ls_v.py`를 보면 `DOMAIN`/`DOMAIN_WS`만 오버라이드하고 `oauth()`는 상속받는데, 모의투자 서버가 실전과 다른 scope 처리를 요구하는지 확인이 필요하다(문제가 없다면 무시해도 됨 — 다만 EBest/LS/Kiwoom 각 모의 서버별 OAuth 파라미터 차이를 점검할 가치가 있다). ## 3. 에러 처리 부재 — 트레이딩 라이브러리로서 가장 큰 리스크 거의 모든 브로커 메서드가 다음 패턴을 따른다: ```python response = requests.post(url, headers=header, data=json.dumps(body)) res = response.json() # ← HTTP 에러/타임아웃/비-JSON 응답이면 여기서 그대로 예외 전파 if res['rsp_cd'] == "00000": # ← 키가 없으면 KeyError ... return ohlcvs # else: 암묵적으로 None 반환 — 실패 사유를 알 수 없음 ``` - **`requests` 호출에 `timeout`이 전혀 지정되지 않았다.** 브로커 서버가 응답을 지연하면 스레드가 무기한 블록된다. 실거래 봇에서는 치명적이다. 최소 `timeout=(3, 10)` 같은 값을 전역 상수로 정해 모든 호출에 적용해야 한다. - **HTTP 상태 코드/네트워크 예외를 확인하지 않는다.** `LS`, `Kis`의 대부분 메서드는 `response.status_code`도 안 보고 바로 `.json()`을 호출한다(반면 `Kiwoom`은 `status_code == 200` 체크를 하는 편이라 상대적으로 낫다). 브로커 서버가 502/503을 반환하거나 HTML 에러 페이지를 주면 `JSONDecodeError`가 호출자에게 그대로 전파된다. - **실패 시 반환값이 전부 `None`(혹은 `[]`) 이고 원인 구분이 불가능하다.** "종목이 없어서 결과가 비었다"와 "네트워크 오류로 실패했다"와 "토큰 만료로 인증 실패했다"를 호출자가 구분할 방법이 없다. **조치 제안**: 1. `finestock/comm/errors.py`에 `FinestockAPIError`, `FinestockAuthError`, `FinestockNetworkError` 같은 예외 계층을 만든다. 2. `API` 베이스에 공통 요청 헬퍼(`_post_json(url, headers, body, timeout=...)`)를 추가해 timeout 지정, HTTP 상태 확인, JSON 파싱 실패 처리, 표준 로깅을 한 곳에서 담당하게 하고, 각 브로커는 이 헬퍼를 호출하도록 리팩터링한다. 3. 실패 시 `None`을 반환하는 대신 예외를 던지거나, 최소한 로그 레벨을 `error`로 남겨 원인이 추적 가능하게 한다. ## 4. 심한 코드 중복 — 유지보수 비용 `finestock/ls/ls.py`의 거의 모든 메서드(`get_ohlcv`, `get_ohlcv_min`, `get_index`, `get_index_min`, `get_index_list`, `get_stock_list`, `get_news_list`, `get_condition_list`, `get_condition_price`, `get_orderbook`, `get_balance`, `get_holds`, `do_order`, `get_order_status`, `do_order_cancel`)가 다음 5줄짜리 보일러플레이트를 그대로 반복한다: ```python response = requests.post(url, headers=header, data=json.dumps(body)) res = response.json() logger.debug(f"[API: oauth]\n" # ← 메서드 이름과 무관하게 항상 "oauth"라고 찍힘(복붙 흔적) f"[URL: {url}]\n" f"[header: {header}]\n" f"[param: {body}]\n" f"[response: {res}]") ``` 로그 라벨이 실제 호출과 무관하게 전부 `[API: oauth]`로 찍히는 것도 복붙의 흔적이며, 로그로 문제를 추적할 때 오히려 혼선을 준다. `Kiwoom` 쪽도 `header = {..., "authorization": ..., "api-id": tr_code, "cont-yn": ..., "next-key": ...}` 패턴과 `response.status_code == 200` → `try: res = response.json() ...` 블록이 `_get_chart_sync`, `get_stock_list`, `get_index_list`, `get_orderbook`, `do_order`, `do_order_cancel`, `get_balance`, `get_holds`에서 거의 동일하게 반복된다. **조치**: 브로커별로 `_request(tr_cd_or_api_id, block_key, body) -> dict` 형태의 공통 헬퍼를 만들어 URL 조립, 헤더 구성, 요청, 로깅, 에러 처리를 한 곳으로 모은다. TR별 바디 구성과 응답 파싱만 각 메서드에 남기면 코드량이 절반 이하로 줄고, 3장의 에러 처리 개선도 자연히 여기에 녹여 넣을 수 있다. ## 5. 인터페이스(계약) 불일치 `api_interface.py`가 정의한 추상 메서드 시그니처와 실제 구현이 어긋나는 곳들: - `MarketDataProvider.get_stock_list(self, mrkt_tp: str = "0")` — `LS.get_stock_list(self)`는 인자를 아예 받지 않는다(`ls.py:301`). `Kiwoom.get_stock_list(self, mrkt_tp="0")`만 계약을 지킨다. - `RealtimeProvider.recv_trade(self, code, status=True)`는 `async` 추상 메서드인데 `LS.recv_trade(self, code)`(`ls.py:731`)는 **동기 함수**이고 `status` 인자도 없다. `await api.recv_trade(...)`로 호출하면 코루틴이 아니라서 `TypeError`가 난다. - `Kiwoom.do_order` / `Kiwoom.do_order_cancel`은 `finestock.Order` 데이터클래스가 아니라 브로커 원본 응답 `dict`를 그대로 반환한다. `LS`/`Kis`는 `finestock.Order`를 반환하므로, `Kiwoom`으로 브로커를 바꾸는 순간 호출부 코드가 깨진다 — 파사드 패턴의 "브로커를 갈아끼워도 동일하게 동작"이라는 목적이 깨지는 지점이다. - `KisV.do_order_cancle`(오타, 스텁)은 CLAUDE.md에도 이미 기록되어 있지만 아직 수정되지 않았다. `finestock.model.flag.TRADE_FLAG.CANCLE`도 동일한 오타가 enum 멤버명에 박혀 있어(`flag.py:6`) 모든 브로커 구현이 이 오타를 그대로 참조하고 있다 — 지금 고치면 파급 범위가 크므로, `CANCEL`을 별도 별칭으로 추가하고 `CANCLE`을 deprecated 처리 후 다음 메이저 버전에서 제거하는 단계적 마이그레이션을 권장한다. **조치**: 브로커 간 치환 가능성이 핵심 가치이므로, 위 불일치들을 CI에서 잡을 수 있도록 6장의 계약 테스트를 도입하는 것이 근본적 해법이다. ## 6. 테스트 커버리지 - `tests/test_model.py`는 `Price` 데이터클래스 생성 하나만 검증한다. `LS`/`Kis`/`Kiwoom`의 JSON 파싱 로직(`_parse_ohlcv`, `_parse_orderbook`, `_parse_real_price` 등)은 테스트가 전혀 없다 — 5장에서 지적한 회귀들이 아무 신호 없이 계속 잠재해 있던 이유이기도 하다. - `requests`/`websockets` 호출을 `unittest.mock`으로 가로채는 테스트가 없어, 브로커가 응답 스키마를 바꿔도 감지할 방법이 없다. - CI 설정(`.github/workflows/*.yml`)이 없어 PR/push 시 `python -m unittest discover tests`조차 자동 실행되지 않는다. **조치**: 1. 각 브로커의 대표 응답 JSON을 fixture로 저장하고, `requests.post`/`.get`을 mock으로 대체해 파싱 로직만 검증하는 단위 테스트를 추가한다. 2. `BaseProvider`의 각 추상 메서드에 대해 "모든 구현체가 동일한 시그니처인지" 확인하는 계약 테스트(예: `inspect.signature` 비교)를 추가해 5장 같은 회귀를 CI에서 잡는다. 3. GitHub Actions로 `python -m unittest discover tests`를 push/PR마다 실행한다. ## 7. 로깅/디버그 잔재 정리 - `print()`와 `loguru.logger`가 뒤섞여 있다(`LS.__init__`의 `print("create LS Components")`, `Kis`/`KisV`의 여러 `print(res)`, `Kiwoom.get_balance`의 `print(response.text)` / `print(res)`). 특히 `Kiwoom.get_balance`가 계좌 잔고 원본 응답을 `print`로 stdout에 그대로 흘리는 것은 1.2와 같은 맥락의 정보 노출이다. - `__del__`에서 `logger.debug(...)`를 호출하는 패턴(`API`, `LS`)은 인터프리터 종료 시점에 모듈 전역이 이미 해제되어 있을 수 있어 예외를 유발할 수 있다. 일반적으로 `__del__`에 로깅/IO를 넣는 것은 권장되지 않는다 — 필요하다면 명시적 `close()`/context manager 패턴으로 대체하는 것이 안전하다. **조치**: 모든 `print`를 `logger.debug`/`logger.info`로 통일하고, 민감 데이터(잔고, 토큰, 키)는 로그에서 마스킹한다. ## 8. 패키징 / 배포 메타데이터 - `setup.py`의 `install_requires`에 `asyncio`가 포함되어 있다. `asyncio`는 Python 3.4+ 표준 라이브러리이며 PyPI 패키지로 명시하면 안 된다(과거 PyPI에 이름이 겹치는 악성 패키지가 올라온 사례도 있었다). 제거해야 한다. - `version='1.0.1.0'`은 4-part 버전으로 PEP 440 기준 `1.0.1.0`도 파싱은 되지만 관례상 `MAJOR.MINOR.PATCH` 3-part(semver 유사)를 쓰는 것이 `CHANGELOG.md`와 대응시키기 쉽다. - `python_requires='>=3.6'` / `classifiers`에 `3.6~3.9`만 나열되어 있지만, 리포지토리의 `__pycache__`에는 `cpython-310/311/312` 산출물이 있어 실제 개발/검증은 3.10~3.12에서 이뤄지고 있는 것으로 보인다. 코드 안에서 f-string(3.6+)과 `dataclass(frozen=True)`(3.7+)를 쓰므로 최소 버전을 3.7~3.8 이상으로 올리고, 실제 CI로 검증한 버전만 classifiers에 남기는 것을 권장한다. - `setup.py`만 있고 `pyproject.toml`이 없다. 최신 packaging 관례(PEP 517/518)를 따르는 `pyproject.toml` 도입을 검토할 만하다. ## 9. 문서 - `README.md`의 "2. 실시간 데이터" 섹션에서 ` ```python ` 코드 펜스가 닫히지 않은 채 "3. 타입 힌팅 활용" 섹션으로 이어지고, 그 뒤에 다시 ` ```python `이 열려 파일 끝의 ` ``` ` 한 줄과 짝이 맞지 않는다(`README.md:67~97`). 현재 GitHub에서 렌더링하면 3번 섹션 전체가 코드 블록으로 표시될 가능성이 높다. - 목차에 `5. License` 항목이 있지만 실제 License 섹션 본문이 파일에 없다 — 링크가 깨진 앵커로 남아있다. 라이선스 파일(`LICENSE`)도 리포지토리에 없다. 배포 패키지로서 라이선스 명시가 필요하다. - README가 LS/KIS 예제만 보여준다. 최근 추가된 Kiwoom 브로커(`finestock/kiwoom/`)에 대한 사용 예시가 없어, `CLAUDE.md`가 설명하는 4개 브로커 지원 범위와 README의 소개 범위("LS, KIS")가 어긋난다. ## 요약: 우선순위별 실행 순서 | 우선순위 | 항목 | |---|---| | 1 (즉시) | 1.1 노출된 실키/토큰 폐기 및 `.gitignore` 보강, 1.2 로그 마스킹 | | 2 | 2.1 KIS 매도 TR ID 공백 버그, 2.2 KIS 기본 인자 고정 버그 | | 3 | 0.2 TR ID/설정 데이터화, 0.3 트랜스포트 계층 도입 → 3장 공통 요청 헬퍼(timeout, 에러 처리, 예외 계층)와 통합 | | 4 | 0.1 인터페이스 mix-in 구조 전환, 5장 인터페이스 불일치 해소 + 6장 계약/파싱 테스트 및 CI 도입 | | 5 | 4장 중복 제거 리팩터링(0.2와 연동), 7장 로깅 정리, 0.5/0.6 동기·비동기 경계·재연결 설계 | | 6 | 0.4 설정 객체 정적 타입화, 8장 패키징 정리, 9장 README/LICENSE 정비 |