본문으로 건너뛰기

KIS 토큰 만료

한국투자증권(KIS) OpenAPI는 OAuth Access Token이 24h 만료입니다. quant-ai의 KISTokenManager가 만료 직전(보통 23h 시점)에 자동 재발급을 시도하지만, 다음 상황에서 갱신이 실패하고 401 / Invalid token 에러가 발생합니다.

사전 요구 사항

  • KIS OpenAPI 신청 완료 + App Key / App Secret 발급됨
  • FEATURE_KIS_BROKER=true
  • 사용자별 키가 exchange_keys 테이블에 등록됨 (asset_class='kr_equity')

KISTokenManager 동작

1. 첫 호출 시 token이 없거나 expired → POST /oauth2/tokenP
2. 응답 access_token + expires_in (24h, 86400초)
3. 메모리 캐시 + DB의 broker_token_cache (있을 경우)
4. 만료 1h 전부터 백그라운드 재발급 시도
5. 재발급 실패 시 다음 API 호출에서 fail-fast (401)

토큰은 사용자별이 아니라 (App Key, App Secret) 페어 단위로 발급됩니다. 즉 같은 키를 공유하는 두 사용자가 있으면 한쪽 갱신이 양쪽에 적용됩니다 — 운영상 사용자별로 별도 App Key를 권장.

1. 진단

# 1) 최근 KIS 401 이벤트
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT received_at, user_id, payload->>'broker' AS broker,
payload->>'http_status' AS code,
LEFT(payload->>'error', 100) AS err
FROM broker_order_events
WHERE payload->>'broker' = 'kis'
AND event_type IN ('error_4xx','auth_failed')
AND received_at > NOW() - INTERVAL '2 hours'
ORDER BY received_at DESC LIMIT 10;
"

# 2) API 로그에서 토큰 갱신 시도
docker compose logs api --tail=500 | grep -E "kis_token|tokenP"

# 3) 사용자별 KIS 키 활성 여부
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT id, user_id, is_active, created_at, last_used_at
FROM exchange_keys
WHERE exchange = 'kis' AND is_active = true;
"

2. 자주 마주치는 케이스

Case A: 자동 재발급이 정상 동작 중 (대기만 하면 OK)

증상: 1~2분 동안 401, 그 후 정상 복귀.

원인: 만료 직후 첫 요청에서 fail → 즉시 재발급 → 다음 요청부터 OK. 정상.

조치: 없음. 단, 연속 발생하면 Case B/C로 이동.

Case B: App Secret 회전 / KIS 측 키 무효화

증상: 갱신 시도가 401로 끝남. 로그에 KIS_INVALID_APP_KEY.

진단:

docker compose exec api python -c "
import requests, os
r = requests.post(
f'{os.environ[\"KIS_BASE_URL\"]}/oauth2/tokenP',
json={
'grant_type': 'client_credentials',
'appkey': os.environ['KIS_APP_KEY'],
'appsecret': os.environ['KIS_APP_SECRET'],
},
timeout=10,
)
print(r.status_code, r.text[:300])"

조치: KIS 콘솔에서 새 키 발급 → .env 또는 사용자 UI에서 갱신.

Case C: 모의투자 ↔ 실투자 URL 미스매치

증상: KIS_INVALID_APP_KEY 에러인데 키는 정상.

진단:

grep KIS_BASE_URL .env
# 모의: https://openapivts.koreainvestment.com:29443
# 실투자: https://openapi.koreainvestment.com:9443

조치: 사용자 의도(모의/실투)에 맞게 URL 변경 후 compose restart api.

Case D: 토큰 캐시 손상

증상: 갱신은 성공하는데 직후 호출이 401.

진단:

# DB 캐시 확인 (broker_token_cache 테이블이 있는 deployment 한정)
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT broker, expires_at, LENGTH(token) AS token_len
FROM broker_token_cache
WHERE broker = 'kis';
"

조치: 캐시 강제 무효화 후 다음 호출에서 재발급:

docker compose exec timescaledb psql -U quant -d quantai -c "
DELETE FROM broker_token_cache WHERE broker = 'kis';
"
docker compose restart api

Case E: hashkey 엔드포인트 분리

증상: 주문 요청만 401, 조회는 정상.

원인: KIS 일부 엔드포인트는 별도 hashkey 서명 필요. KIS_HASHKEY_URL 미설정.

조치: .envKIS_HASHKEY_URL 추가하거나 KIS 측 권장 기본 사용.

Case F: 동시 요청 폭주

증상: 1초에 수십 건 요청 후 일부가 401.

원인: KIS rate limit (초당 20건 등). 토큰은 정상이지만 거래소 측 제한.

조치: RECONCILER_MAX_CONCURRENT_USERS 또는 broker client의 동시 요청 한도 하향. 자세한 건 포지션 리컨실러.

3. 검증

# 단일 호출 성공 확인
docker compose exec api python -c "
from src.execution.brokers.kis_broker import KISBroker
b = KISBroker.from_env()
print(b.get_account_balance()) # 200 응답이어야 함
"

또는 사용자 UI에서 "한국투자증권 계좌 정보" 페이지 새로고침 → 잔고가 표시되면 정상.

4. 운영 팁

  • 사용자별 키 권장 — 한 키를 다수 사용자가 공유하면 토큰 갱신이 race condition으로 중복 발급되어 KIS rate limit에 걸릴 수 있음.
  • 모의투자 / 실투자 키는 별도 — 같은 App Key가 양쪽에서 작동하지 않음.
  • 갱신 실패 시 큐 / 봇 매니저는 다음 사이클까지 기다리지 않고 즉시 fail — 사용자에게 빠른 피드백.
  • KIS는 브로커 5xx 알림 트리거에서 제외할 가치 있음 — 자체 정비 시간이 잦아 false positive가 많음. 단, 룰을 영구 변경하기 전 디자인 리뷰.

트러블슈팅

증상원인 / 조치
24h마다 정확히 1~2분 401자동 재발급 정상. 무시 가능
매시간 401KIS 측 토큰 강제 만료 (정비) — KIS 공지 확인
영구 401키 무효 / URL 미스매치 → Case B/C
갱신 자체가 안 일어남API 컨테이너 정지 / 백그라운드 태스크 죽음. docker compose restart api

관련 페이지