주문은 나갔고, 체결도 됐다
매도 주문 하나가 오후 1시 12분에 체결됐다. 증권사 앱을 열어보면 체결 내역이 분명히 있었다.
그런데 자동매매 앱은 그날이 끝날 때까지 그 사실을 몰랐다. 포지션은 여전히 보유 중으로 남아 있었고, 실현 손익에도 잡히지 않았다. 앱 입장에서 그 주문은 발사했는데 응답이 없는 상태였다.
주문 접수 응답에는 주문번호가 정상으로 들어 있었다. 문제는 그다음이었다. 그 주문번호로 체결 내역을 조회하는 단계에서 HTTP 404가 돌아왔다.
404는 원인을 말하지 않는다
체결 조회에서 404가 뜨면 후보가 한둘이 아니다.
- 주문번호가 아직 조회 인덱스에 안 올라갔다
- 조회에 필요한 부가 파라미터(지점 코드 등)가 빠졌거나 틀렸다
- 요청 경로나 거래 ID가 실전 환경과 모의 환경 사이에서 어긋났다
- 토큰이 만료됐다
어느 쪽이든 응답 본문은 똑같이 빈약하다. 그리고 이것이 진짜 문제였다. 당시 코드는 404를 받으면 조회 실패로만 처리하고 다음 분기로 넘어갔다. 재시도를 두 번 했고 두 번 다 404였다는 것만 남았다. 무엇을 어디로 보냈길래 404가 났는지가 어디에도 기록되지 않았다.
며칠 뒤에 원인을 찾으려 해도 재료가 없다. 재현하려면 같은 상황이 다시 오기를 기다려야 하는데, 그 상황은 실전 매매 중에만 온다.
그래서 먼저 고친 것은 로직이 아니라 로그였다
원인을 모르는 채로 코드를 고치면 추측으로 고치게 된다. 그래서 순서를 바꿨다. 다음에 같은 404가 나면 그 자리에서 원인이 드러나도록 만드는 것이 먼저다.
체결 조회가 성공 응답이 아닌 것을 받으면 아래를 남기게 했다.
- 요청 URL과 거래 ID
- 조회 대상 종목 코드, 주문번호, 지점 코드
- 응답 본문 원문 그대로
마지막 항목이 핵심이다. 파싱한 결과가 아니라 원문이어야 한다. 파싱은 응답이 예상 형태일 때만 성공하는데, 지금 문제는 응답이 예상 형태가 아니라는 것이다.
로그에 절대 넣지 않은 것
같은 요청 안에 있지만 출력에서 제외한 값들이 있다.
- 접근 토큰
- 앱 키, 앱 시크릿
- 계좌번호와 상품 코드
진단에 필요하지 않으면서 유출 시 피해가 큰 값들이다. 디버깅이 급하다는 이유로 요청 객체를 통째로 덤프하지 않는다. 한 번 파일에 남으면 그 파일은 백업으로, 압축본으로, 공유 폴더로 계속 복제된다.
두 번째 경로도 함께 계측했다
개별 주문번호 조회가 실패했을 때 쓰는 폴백이 있다. 당일 체결 목록을 통째로 받아서 그 안에서 주문번호를 찾는 방식이다. 개별 조회가 404여도 목록 조회는 되는 경우가 있어서 안전망 역할을 한다.
그런데 이 폴백도 5분 넘게 주문번호를 못 찾았다. 목록에 그 주문이 없었는지, 있었는데 매칭 조건이 어긋났는지 알 수 없었다. 여기도 응답을 안 남기고 있었기 때문이다.
그래서 목록 조회 직후에도 남기게 했다.
- 받은 항목 개수
- 앞쪽 몇 건의 원본 필드(주문번호, 종목코드, 매수매도 구분, 체결 수량, 체결 시각)
이건 정상일 때도 매번 찍히므로 DEBUG 레벨로 내렸다. 평소에는 안 보이고, 문제가 생겼을 때 로그 레벨만 올리면 보인다. 운영 로그를 조용하게 유지하면서 필요할 때 재료를 확보하는 절충이다.
왜 개수와 앞쪽 몇 건인가
전량을 찍으면 하루에 수백 줄이 쌓이고, 그러면 아무도 안 읽는다. 반대로 개수만 찍으면 0건과 매칭 실패를 구분할 수 없다.
- 개수가 0 → 목록 조회 자체가 비어서 왔다. 조회 조건이나 API 쪽 문제
- 개수는 있는데 매칭 실패 → 목록에 데이터는 왔다. 우리 매칭 로직이 필드를 잘못 보고 있다
이 둘은 고쳐야 할 곳이 완전히 다르다. 앞쪽 몇 건의 원본 필드가 있으면 어느 쪽인지 한눈에 갈린다. 필드명이 예상과 다르거나 값 형식이 다른 것도 거기서 드러난다.
검증은 어떻게 했나
이런 종류의 계측은 코드를 넣었다로 끝내면 안 된다. 정말 찍히는지를 봐야 한다.
- 실패 경로에 도달하는 단위 테스트를 만들어 로그 호출이 일어나는지 확인
- 민감 필드가 출력 문자열에 포함되지 않는지 별도 검증
- 다음 실거래 가동에서 자연 발생하는 정상 응답으로 DEBUG 출력 형태 확인
세 번째가 중요하다. 정상 응답의 필드 구조를 미리 봐두면, 나중에 비정상 응답이 왔을 때 무엇이 다른지 즉시 비교할 수 있다.
남는 이야기
이 사고에는 결함이 하나 더 있었다. 체결 확인이 실패하면서 앱이 매도 진행 중 상태를 풀어버렸고, 같은 매도 주문이 반복해서 나갔다. 증권사는 그때마다 수량 초과로 거부했다.
그쪽은 조회 API가 아니라 주문을 막는 가드의 문제라 별도로 다룬다.
정리하면, 외부 API가 원인을 설명하지 않는 응답을 줄 때 가장 먼저 할 일은 로직 수정이 아니라 다음번에 원인이 드러나게 만드는 것이다. 요청과 응답 원문을 남기되 자격 증명은 빼고, 정상 경로는 DEBUG로 내려 평소에는 조용하게 둔다. 추측으로 고친 코드는 다음에 같은 증상이 왔을 때 무엇이 효과가 있었는지도 알려주지 않는다.