자본시장과 결제 · 주문의 생애주기와 시계 · 이론
주문은 상태가 아니라 이벤트의 줄이다
한 줄 요약
주문의 최종 상태는 어딘가에 저장된 필드가 아니라 여러 이벤트를 순서대로 접어 만든 결과이고, 접는 규칙과 접는 순서가 정해져 있지 않으면 같은 로그에서 서로 다른 답이 나온다.
왜 이게 필요했나
증권사 지원 시스템에 들어가면 거의 예외 없이 이 문장으로 시작합니다. "체결 통보는 받았는데 화면에는 취소로 떠 있어요."
처음 이 말을 들으면 화면이 잘못됐거나 DB 가 늦게 갱신됐다고 생각합니다. 그래서 orders 테이블을 열어 status 열을 봅니다. 거기에는 cancelled 라고 적혀 있습니다. 그 다음에 할 일이 무엇인지 모르면 조사는 여기서 멈춥니다.
멈추는 이유는 주문 상태를 하나의 값이라고 믿었기 때문입니다. 실제로는 이렇게 생겼습니다.
09:31:04.118 new 수량 300, 지정가 71,50009:31:04.180 ack 거래소가 접수를 확인09:44:12.007 partial_fill 150주 체결, 누적 15009:47:55.310 cancel_req 잔량 취소 요청09:47:55.402 fill 150주 체결, 누적 30009:47:55.408 cancelled 잔량 취소 확정이 여섯 줄에서 "이 주문은 무엇인가" 를 뽑아내야 합니다. 마지막 이벤트만 보면 취소입니다. 체결 수량만 보면 전량 체결입니다. 둘 다 이 로그에 있는 사실이고, 어느 쪽을 상태로 부를지는 우리가 정하는 규칙입니다. 규칙을 명시적으로 정하지 않은 시스템은 코드 여러 곳이 각자 다른 규칙을 쓰고 있고, 그래서 화면과 통보 메일과 정산 배치가 서로 다른 답을 냅니다.
어떻게 동작하나
이벤트를 접는 함수는 대략 이렇게 생깁니다. 여기서 중요한 것은 코드가 아니라 각 줄이 어떤 판단을 담고 있는가입니다.
new 주문 수량이 정해진다. 상태는 '접수'ack 거래소가 받았다. 상태는 '유효'reject 거절. 여기서 끝난다replace 주문 수량이 바뀐다. 상태는 그대로cancel_req 아무것도 바꾸지 않는다 ← 여기가 첫 번째 함정cancelled 상태는 '취소'fill 계열 누적 체결에 더하고, 주문 수량에 닿으면 '체결'cancel_req 가 상태를 바꾸지 않는 것이 핵심입니다. 취소를 요청한 것과 취소가 된 것은 다른 사실입니다. 요청은 우리가 보낸 것이고 확정은 거래소가 준 것입니다. 요청 시점에 상태를 cancelled 로 바꾸는 구현이 실제로 흔한데, 그렇게 하면 요청 뒤에 체결이 붙은 주문이 취소로 남습니다. 고객은 주식을 받았는데 우리 장부에는 없습니다.
두 번째 함정은 부분체결이 여러 번 온다는 것입니다. 300주 주문이 150 + 100 + 50 으로 세 번에 나뉘어 체결되는 것은 이상한 일이 아니라 기본입니다. 각 체결은 자기 몫의 수량만 담고, 누적은 우리가 세야 합니다. 거래소가 cum_qty 를 같이 주기도 하지만 그 값이 우리가 센 값과 다르면 그 자체가 사고 신호입니다.
세 번째 함정은 같은 체결이 두 번 온다는 것입니다. 세션이 끊겼다 붙거나 재전송을 요청하면 이미 받은 체결이 다시 옵니다. 그래서 체결마다 유일한 exec_id 가 붙어 있고, 그것으로 걸러 내는 것은 받는 쪽 책임입니다. 거르지 않으면 누적 수량이 부풀고, 주문 수량을 넘긴 것은 눈에 띄지만 넘지 않은 것은 아무 경보 없이 틀린 채로 남습니다.
현장에서 만나는 모습
한 증권사에서 야간 정산 배치가 매일 새벽에 실패했습니다. 로그에는 "체결 수량이 주문 수량을 초과" 라는 메시지가 여섯 건 있었고, 담당자는 매일 아침 그 여섯 건을 손으로 고쳐 넣고 있었습니다. 여섯 달째였습니다.
원인은 재전송된 체결을 거르지 않은 것이었습니다. 그리고 진짜 문제는 그 여섯 건이 아니었습니다. 같은 원인으로 수량이 부푼 주문이 스물몇 건 더 있었는데, 그것들은 주문 수량을 넘기지 않아서 아무 메시지도 남기지 않았습니다. 매일 아침 고치던 여섯 건은 빙산의 꼭대기였고, 아래쪽은 여섯 달 동안 아무도 세지 않았습니다.
또 하나 자주 만나는 모습은 상태 필드와 이벤트 로그가 갈라진 채로 몇 년을 가는 것입니다. 상태 필드는 실시간 처리기가 갱신하고 이벤트 로그는 원본 그대로 쌓이는데, 처리기가 한 번 이벤트를 놓치면 그 주문의 상태 필드는 영원히 틀립니다. 이벤트는 다시 흘러오지 않고, 상태 필드를 다시 계산하는 절차는 대개 없습니다. 그래서 조사할 때는 상태 필드를 믿지 말고 이벤트에서 다시 만들어 보는 것이 첫 수순입니다.
그다음 실습에서 할 것
먼저 다음 이론에서 시계와 순서를 보고, 그 뒤 실습으로 갑니다.
주문 900건의 이벤트 로그를 직접 만들고, 그 로그만 가지고 주문별 최종 상태를 재구성합니다. 그다음 OMS 가 들고 있는 상태 필드와 대조해 어긋난 주문을 찾습니다. 상태 문자열만 비교했을 때와 체결 수량까지 비교했을 때 나오는 숫자가 다르고, 그 차이가 이번 사고의 본체입니다.