KIS(한국투자증권)에서 3주 넘게 안 나던 종류의 버그가 NH증권 Open API를 붙이자마자 연달아 터진 이유
주계좌 증권사를 KIS에서 NH로 옮기는 작업을 하고 있습니다. 그런데 신기한 일이 하나 있었습니다. 이전 몇 주 동안 KIS 쪽에서는 거의 안 나던 종류의 사고가, NH를 붙이자마자 거의 매주 하나씩 터졌습니다.
같은 걸 하는 API인데 왜 한쪽에서만 이런 일이 반복될까요? 이번 글은 그 이유를 정리한 것입니다.
왜 증권사를 옮기게 됐나
원래는 KIS(한국투자증권) API로만 자동매매를 돌리고 있었습니다. 그런데 주계좌를 NH로 옮기기로 하면서, 실계좌 전환에 앞서 검증용으로 NH 모의투자 슬리브를 하나 병행 운영하기 시작했습니다. 처음엔 "그냥 API 하나 더 붙이는 일"이라고 가볍게 생각했습니다.
착수하자마자 자문 결과가 나왔습니다. "내일 바로 실계좌 전환"은 안 된다는 판정이었습니다. 이유는 단순했습니다. 실주문 경로가 아직 없었고, 그 계좌엔 제가 원래 갖고 있던 다른 종목들도 섞여 있어서 봇이 제 보유주식을 잘못 건드릴 위험이 있었고, 무엇보다 NH 쪽으로 검증된 표본이 하나도 없었습니다. 그래서 모의투자 병행 → 실계좌 소액 시험 → 예산 단계적 이관 → KIS 은퇴, 이렇게 단계를 밟기로 했습니다.
그 "검증 표본을 쌓는" 구간에서 문제가 몰아쳤습니다.
3주 동안 NH에서만 터진 것들
전부 나열하면 지루하니, 성격이 다른 몇 가지만 짚어보겠습니다.
체결가가 조용히 1000분의 1로 줄어서 왔습니다. NH의 체결 조회 API가 평균체결단가 필드를 실제보다 1000배 작게 돌려주는 걸 한동안 몰랐습니다. 처음 한 번 나왔을 땐 "어쩌다 한 번이겠지" 하고 넘겼는데, 알고 보니 그날 이후 발생한 체결 전부에서 상시로 벌어지고 있었습니다. 이 오염된 값이 원장에 그대로 쌓이면서 매수 대금이 실제보다 작게 기록됐고, 사흘 뒤 안전장치가 이상 신호를 감지하고 자동으로 거래를 멈췄습니다. 다행히 돈이 잘못 나가진 않았지만, 원장을 손으로 다시 맞추는 작업이 필요했습니다.
분명히 산 종목인데 보유 조회에는 안 잡혔습니다. 체결 내역엔 매수가 정상적으로 찍혀 있는데, 보유 종목을 조회하면 그 종목이 아예 없는 것처럼 나왔습니다. 알고 보니 보유 목록 API가 응답을 여러 페이지로 나눠 주는데, 첫 페이지만 읽고 끝내던 게 문제였습니다. 보유 종목이 한 페이지 분량을 넘어가면 뒤쪽 종목이 조용히 누락된 겁니다. "장부엔 있는데 증권사엔 없다"로 보였으니, 안전장치 입장에서는 장부가 오염됐다고 판단해 거래를 멈춘 게 당연했습니다.
같은 페이지네이션 문제가 실전 계좌와 모의 계좌에서 다르게 동작했습니다. 위 문제를 고친 뒤 더 깊이 파보니, 실전 계좌 쪽은 페이지가 끊겨도 "다음 페이지 있음" 신호를 정직하게 주는데, 모의투자 쪽은 같은 상황에서 "다음 페이지 없음"이라고 거짓으로 알려준다는 걸 확인했습니다. 겉보기엔 같은 API처럼 문서화돼 있었지만, 실제로는 실전과 모의가 서로 다른 뒷단(백엔드)으로 돌아가고 있었던 겁니다. 모의투자에서 아무리 검증해도 실전에서 똑같이 작동한다는 보장이 없다는 뜻이라, 이 발견 이후로는 검증 원칙 자체를 다시 정했습니다.
아무 공지 없이 특정 API가 막혔습니다. 며칠 전까지 잘 되던 현재가 조회가 어느 날 갑자기 "모의투자에서는 제공하지 않는 기능"이라는 에러를 내기 시작했습니다. 문서가 바뀐 것도 아니고 공지가 온 것도 아니었습니다. 다른 시세 제공처로 조용히 우회하는 보완 경로를 급히 붙였습니다.
토큰이 만료 시각보다 먼저 무효화됐습니다. 로컬에 캐시해 둔 인증 토큰이 아직 유효기간이 한참 남았다고 믿고 있었는데, 서버는 이미 그 토큰을 무효 처리한 상태였습니다. 그 바람에 그날 오후 거래가 통째로 조용히 멈췄습니다. 지금은 "토큰이 무효하다"는 응답을 받으면 캐시를 버리고 즉시 재발급받도록 고쳤습니다.
같은 버그가 다른 API에서 또 나왔습니다. 보유 조회에서 겪었던 페이지네이션 문제를, 이번엔 자산현황 조회 API에서 똑같이 겪었습니다. 심지어 이번엔 중간 페이지의 합계 항목이 전부 0으로 채워져 있어서, 한 페이지만 읽으면 계좌 평가금액이 0원인 것처럼 보이는 조금 더 짓궂은 버전이었습니다. 실계좌에서 발생했다는 점이 특히 뼈아팠습니다.
같은 종류의 오탐이 2주 동안 9번 반복됐습니다. "장부엔 있는데 증권사 조회엔 없다"는 패턴이 계속 반복돼서 처음엔 "NH 서버가 원래 이렇게 불안정한가 보다"라고 받아들이고 있었습니다. 그런데 다시 들여다보니 진짜 원인은 우리 쪽 코드였습니다. 보유 조회 함수가 페이지를 읽다가 중간에 실패해도 예외를 던지지 않고 그때까지 읽은 부분 결과를 "성공"인 것처럼 돌려주고 있었습니다. 그 부분 결과의 누락분을 "증권사에 진짜로 없다"고 오해한 거였습니다. "그쪽 서버가 이상하다"는 첫 가설이 실은 우리 코드가 API 계약을 잘못 읽은 것이었던 셈입니다.
왜 KIS에서는 안 나던 일이 NH에서만 났을까
몇 가지 구조적인 차이가 겹쳤습니다.
- 문서의 출발선이 달랐습니다. KIS는 정식 개발자 문서가 있었지만, NH는 공식 문서가 충분치 않아서 실제 요청/응답을 관찰하며 역으로 스펙을 재구성해야 했습니다. 검증되지 않은 영역이 그만큼 많았고, 그 영역들이 하나씩 실제 사고로 드러났습니다.
- "모의투자는 실전의 축소판"이라는 전제가 안 통했습니다. KIS에서는 대체로 맞는 가정이었는데, NH는 실전과 모의가 사실상 다른 시스템처럼 동작했습니다. 모의에서 안전하다고 확인한 게 실전에는 적용이 안 되는 경우가 있었습니다.
- 페이지네이션 방식이 응답 헤더에 숨어 있었습니다. 응답 본문이 아니라 헤더 안의 특정 값으로 "다음 페이지가 더 있다"를 알려주는 구조였는데, 이걸 놓치기 쉬웠고 한 번 놓치면 여러 API에서 똑같은 실수가 반복됐습니다.
- 필드 하나의 신뢰도가 API마다 달랐습니다. 체결단가처럼 겉보기엔 당연히 맞을 것 같은 필드가 실은 계산이 틀려서 오는 경우가 있었고, 이런 건 문서만 봐서는 절대 알 수 없고 실측으로만 잡아낼 수 있었습니다.
증권사(혹은 외부 금융 API)를 옮길 때 챙겨야 했던 것들
이번에 겪은 걸 정리하면, 다음번에 비슷한 걸 붙이는 사람에게 도움이 될 것 같아 체크리스트로 남깁니다.
- 문서를 믿지 말고 실측하라. 요청 한도, 응답 코드, 성공/실패 판정 기준까지 전부 문서 값과 실제 값을 대조해봐야 합니다. 저희는 요청 한도조차 문서 값과 실측이 달랐습니다.
- 모의(샌드박스) 환경과 실전 환경을 "같은 시스템의 두 모드"라고 가정하지 말라. 같은 API처럼 보여도 뒷단이 완전히 다른 시스템일 수 있습니다. 모의에서 통과했다고 실전 검증을 생략하면 안 됩니다.
- 페이지네이션이 응답 헤더에 있는지부터 확인하라. 본문만 보고 "끝났다"고 판단하면, 데이터가 있는데 없는 것처럼 보이는 조용한 누락이 생깁니다. 이건 한 곳에서 발견됐다고 끝난 게 아니라, 같은 뒷단을 쓰는 다른 조회 API 전부를 의심해야 합니다.
- 조회 함수가 "부분 실패"를 "성공"으로 둔갑시키지 않는지 점검하라. 여러 페이지를 읽다가 중간에 실패했을 때 예외를 던질지, 그때까지 읽은 걸 그냥 돌려줄지는 호출부의 안전 로직을 통째로 좌우합니다. 저희는 이 계약을 잘못 가정해서 "서버가 불안정하다"는 가설로 2주를 허비했습니다.
- 필드 하나하나의 스케일과 단위를 의심하라. 특히 가격·수량처럼 돈이 걸린 필드는, 다른 필드(체결금액÷수량처럼 역산 가능한 값)와 교차검증하는 별도 경로를 만들어두는 게 안전합니다.
- API 정책이 사전 공지 없이 바뀔 수 있다고 가정하고 폴백을 준비하라. 특히 무료/모의 계정 쪽 기능은 언제든 축소될 수 있습니다.
- 인증 토큰의 로컬 만료시각을 서버가 그대로 지켜준다고 믿지 말라. "무효한 토큰"류 에러를 받으면 캐시를 버리고 즉시 재발급하는 경로를 처음부터 넣어두는 게 낫습니다.
- 경보가 뜨면 그날 안에 처리하라. 저희가 겪은 사고 중 가장 피해가 컸던 건 버그 자체보다, 경보가 뜬 뒤 사흘을 방치해서 오염이 계속 쌓인 쪽이었습니다. 버그는 하루짜리 사고로 끝날 수 있었는데, 방치가 그걸 사흘짜리 복구 작업으로 키웠습니다.
일반화하면
새 API를 붙이는 초반엔, 같은 일을 하는 기존 API보다 사고가 훨씬 잦은 게 정상입니다. 검증되지 않은 영역의 크기가 다르기 때문입니다. 이 시기의 사고를 "이 API가 원래 불안정하구나"로 성급하게 결론 내리기보다는, "우리가 아직 이 API의 계약을 다 파악 못 했다"는 신호로 먼저 의심하는 편이 맞았습니다. 실제로 가장 오래 반복된 사고의 진짜 원인은 상대방 서버가 아니라 저희 쪽 호출 코드였습니다.
같은 버그 클래스는 한 곳에서 고쳤다고 끝난 게 아닙니다. 페이지네이션 문제를 한 API에서 고쳐놓고도, 같은 뒷단을 쓰는 다른 조회 API에서 똑같은 문제가 다시 나왔습니다. 한 군데서 구조적 결함을 발견했으면, "이 패턴을 쓰는 다른 곳이 또 있는가"를 그 자리에서 먼저 확인하는 편이 사고가 나고서야 하나씩 대응하는 것보다 훨씬 쌉니다.
돈이 걸린 경보는 방치 자체가 비용입니다. 경보가 뜬 순간의 피해는 대개 작습니다. 그 경보를 며칠 미뤄두는 사이에 오염된 값이 다른 계산에 다시 섞여 들어가면서 피해가 복리로 불어납니다. 경보에 빠르게 반응하는 습관 하나가, 사고 하나를 하루짜리로 끝내느냐 며칠짜리로 키우느냐를 갈랐습니다.
Top comments (0)