시스템설계 워치독은 프로세스를 살릴 뿐이다 — 봇 재시작 안전성 설계
게시물 옵션
본문
봇이 죽는 건 사고다. 하지만 봇이 아무것도 기억하지 못한 채 되살아나는 것은 사고보다 위험할 수 있다. 워치독이 프로세스를 살려내는 순간, 그 봇은 지금 자기가 코인을 들고 있는지조차 모르는 상태로 시장에 다시 뛰어든다.
1. 워치독 다음에 오는 질문
이전 편에서 워치독 패턴을 다뤘다. 봇이 멈추면 감시 프로세스가 감지해서 다시 띄우는 구조다. 그런데 그 글을 쓰고 나서 실제로 운영해 보니, 더 어려운 문제는 그 다음에 있었다. 다시 켜진 봇이 어떤 상태에서 시작하느냐는 것이다.
대부분의 자동매매 봇은 실행 중에 여러 가지를 기억한다. 지금 어떤 코인을 얼마에 샀는지, 손절 라인이 어디인지, 오늘 이미 몇 번 진입했는지, 트레일링 스탑의 최고가가 얼마였는지. 문제는 이 기억이 전부 프로세스 메모리 안에만 있다는 점이다. 프로세스가 죽으면 이 기억도 같이 사라진다.
NAS에서 봇을 돌리다 보면 재시작은 생각보다 자주 일어난다. 도커 컨테이너 재생성, 시스템 업데이트, 네트워크 순단으로 인한 예외 종료, 그리고 필자의 환경에서는 특정 시간대에 발생하는 NAS 시각 동기화 이슈까지. 한 달에 몇 번은 무조건 겪는다고 보면 된다.
2. 상태를 잃은 봇이 만드는 세 가지 사고
① 중복 진입
가장 흔하다. 봇이 BTC를 이미 매수한 상태에서 죽었는데, 재시작 후에는 보유 사실을 모른다. 진입 조건이 여전히 참이면 같은 자리에서 한 번 더 산다. 포지션 사이징을 아무리 정교하게 짜놔도 이 한 번으로 의도한 비중의 두 배가 실린다. 동시보유 종목 수 제한도 같은 방식으로 무력화된다.
② 유령 포지션
반대 방향도 있다. 봇 내부 기록에는 매수 체결로 남았는데 실제 거래소에서는 주문이 취소되거나 부분 체결에 그친 경우다. 봇은 있지도 않은 물량의 손절을 감시하고, 매도 신호가 오면 없는 수량으로 매도 주문을 던진다. 거래소는 잔고 부족 오류를 뱉고, 그 오류를 처리 못 하는 봇은 거기서 다시 죽는다. 죽음이 죽음을 부르는 루프가 만들어진다.
③ 손절 공백
가장 조용하지만 가장 비싸다. 손절 라인이 메모리에만 있었다면, 재시작된 봇은 손절해야 할 포지션을 그냥 방치한다. 하락이 진행되는 동안 아무도 지켜보지 않는 시간이 생긴다. 리스크 관리 편에서 다뤘던 모든 장치가 이 한 번의 재시작으로 무효가 된다.
그림 1. 재시작 시점에 상태가 어디에 있었는지가 결과를 가른다
3. 진실의 출처를 하나로 정한다
해결의 출발점은 단순한 질문이다. "지금 내가 무엇을 들고 있는가"에 대한 정답은 어디에 있는가?
봇 내부 기록과 거래소 잔고가 다를 때, 어느 쪽을 믿을 것인가. 답은 항상 거래소다. 봇의 기록은 거래소 상태에 대한 캐시일 뿐이다. 캐시는 언제든 틀릴 수 있고, 틀렸을 때는 원본을 다시 읽어야 한다.
| 구분 | 신뢰도 | 역할 |
|---|---|---|
| 거래소 잔고 API | 원본(정답) | 기동 시 반드시 조회. 보유 수량·평균단가의 기준 |
| 로컬 DB 기록 | 보조 | 손절가·최고가·진입 시각 등 거래소가 모르는 값 보관 |
| 프로세스 메모리 | 휘발성 | 연산용 임시값. 여기에만 있는 값은 없어도 되는 값이어야 함 |
그래서 봇 기동 루틴의 첫 줄은 전략 계산이 아니라 상태 복원이어야 한다. 순서는 거래소 잔고 조회 → 로컬 DB 조회 → 두 값 대조 → 불일치 시 거래소 기준으로 정정 → 그 다음에 비로소 매매 로직 진입이다.
4. 로컬에 남겨야 하는 것과 남기지 말아야 하는 것
모든 걸 다 저장하려 들면 오히려 복잡해진다. 기준은 이렇다. 거래소에 물어봐서 알 수 있는 값은 저장하지 않는다. 거래소가 모르는 값만 저장한다.
- 저장해야 하는 값 — 손절 가격, 트레일링 최고가, 진입 판단 근거, 진입 시각, 당일 진입 횟수, 마지막 신호 처리 시각
- 저장할 필요 없는 값 — 보유 수량, 평균 매수가, 미체결 주문 목록 (전부 조회 가능)
- 절대 저장하면 안 되는 값 — API 키. 이건 상태가 아니라 자격증명이고, 환경변수나 별도 권한 분리된 경로에 둬야 한다
저장 매체는 거창할 필요 없다. 개인 규모 봇이라면 SQLite 파일 하나로 충분하다. 중요한 건 쓰기 시점이다. 주문을 낸 다음에 기록하는 게 아니라, 주문을 내기 직전에 "지금부터 이 주문을 시도한다"를 먼저 남겨야 한다. 그래야 주문 전송 직후 프로세스가 죽어도 흔적이 남는다.
5. 멱등성 — 두 번 실행돼도 한 번만 반영되게
재시작 안전성의 핵심 개념이 멱등성(idempotency)이다. 같은 작업을 여러 번 실행해도 결과가 한 번 실행한 것과 같아야 한다는 성질이다.
주문에 멱등성을 부여하는 가장 실용적인 방법은 주문마다 고유 식별자를 직접 부여하는 것이다. 많은 거래소 API가 클라이언트 측 주문 ID를 받아준다. 봇이 "2026-09-10 BTC 진입 시도 #1"에 해당하는 결정적(deterministic) 키를 만들어 붙이면, 같은 판단으로 두 번 전송해도 거래소가 중복을 걸러낸다.
거래소가 이 기능을 지원하지 않는다면 차선책은 주문 전 확인이다. 주문을 던지기 전에 최근 체결·미체결 내역을 조회해서, 같은 조건의 주문이 이미 존재하는지 검사한다. 완벽하진 않지만 재시작 직후의 중복 진입 대부분을 막아준다.
6. 정리
- 워치독은 프로세스를 살릴 뿐, 상태는 살리지 못한다
- 재시작한 봇의 3대 사고는 중복 진입, 유령 포지션, 손절 공백이다
- 보유 상태의 정답은 언제나 거래소다. 로컬 기록은 캐시로 취급한다
- 거래소가 모르는 값(손절가·최고가·진입 근거)만 골라서 영속화한다
- 주문 기록은 전송 직전에 남긴다
- 클라이언트 주문 ID로 멱등성을 확보하고, 기동 직후엔 쿨다운을 둔다
자동매매 시스템의 안정성은 전략의 정교함보다 실패했을 때 어떻게 되돌아오는가에서 갈린다. 잘 돌아갈 때의 코드는 누구나 짤 수 있다. 어려운 건 죽었다 깨어나는 코드다.
댓글목록
등록된 댓글이 없습니다.