카테고리 없음

[시즌 3 - 7화] "절대로 잠들지 않는 파수꾼" API 호출 제한(Rate Limit) 방어와 봇 자가 치유(Self-Healing) 프로세스 구축

myaistories 2026. 5. 27. 06:55
반응형

안녕하세요! 지난 연재에서는 여러 자산군에 수학적으로 안전하게 시드를 나누고 분배하는 '정적 켈리 공식 자금 관리 모듈'을 이식하여 리스크 방어망을 견고히 구축했습니다.

이제 트레이딩 봇의 기능적 구현은 완성에 가까워졌지만, 운영 관점에서의 진짜 복병이 남아 있습니다. 바로 **'예외 상황으로 인한 프로세스 셧다운'**입니다. 24시간 작동하는 봇은 아무리 코딩을 완벽하게 했더라도 다음과 같은 현실적인 물리 세계의 에러를 무조건 겪게 됩니다.

  • 거래소 점검으로 인한 일시적인 API 서버 응답 없음 (Request Timeout)
  • 단시간에 너무 자주 가격을 조회하여 거래소로부터 받는 일시적 IP 차단 (HTTP 429 Too Many Requests)
  • 가상 서버(AWS 라이트세일) 내부의 일시적 네트워크 순단

이러한 예외가 발생했을 때 봇이 에러 로그를 뿜으며 그냥 죽어버린다면, 자는 동안 혹은 이동 중에 매매 기회를 모두 잃게 됩니다. 오늘 연재에서는 에러 앞에서도 지치지 않고 버티는 '지수 백오프(Exponential Backoff)' 재시도 패턴과, 프로세스가 죽었을 때 스스로 심폐소생술을 수행하는 **'크론탭 자가치유(Self-Healing) 스크립트'**를 완성해 보겠습니다!


1. API 호출 제한 방어: 지수 백오프(Exponential Backoff) 알고리즘

거래소 API 서버는 무분별한 트래픽 공격을 막기 위해 1초당 요청 횟수를 엄격하게 제한합니다. 이를 초과하면 HTTP 429 또는 DDoS Ban을 날려 일정 시간 동안 봇을 무력화시킵니다.

단순히 에러가 났을 때 while True로 즉시 재시도하면 차단 기간이 더 연장됩니다. 따라서 에러가 날 때마다 재시도 간격을 2배씩 점진적으로 늘려가며(지수적 증가) 대기한 뒤 요청하는 지수 백오프 방식이 글로벌 엔지니어링 표준입니다. 여기에 매번 미세한 랜덤 노이즈(Jitter)를 섞어주어 거래소 서버에 동시 요청이 몰리는 현상도 분산시킵니다.

💻 파이썬 지수 백오프 구현 예제

import time
import random
import ccxt

def call_api_with_backoff(exchange_func, *args, max_retries=5, base_delay=2, **kwargs):
    """
    거래소 API 호출 실패 시 지수 백오프(Exponential Backoff)와 Jitter를 적용하여 재시도합니다.
    - exchange_func: 실행할 CCXT API 함수 (예: exchange.fetch_ticker)
    - max_retries: 최대 재시도 횟수
    - base_delay: 최초 재시도 대기 시간(초)
    """
    retries = 0
    while retries < max_retries:
        try:
            # API 함수 실행
            return exchange_func(*args, **kwargs)
        except (ccxt.NetworkError, ccxt.RequestTimeout, ccxt.DDoSProtection) as e:
            retries += 1
            if retries >= max_retries:
                print(f"[API 경고] 최대 재시도 횟수({max_retries}회)를 초과했습니다. 호출 포기: {e}")
                raise e
            
            # 대기 시간 계산: base_delay * (2 ^ (retries - 1)) + random_jitter
            delay = base_delay * (2 ** (retries - 1))
            jitter = random.uniform(0.1, 1.0)
            final_delay = delay + jitter
            
            print(f"[API 에러] {e} 발생. {final_delay:.2f}초 후 재시도합니다. (시도 {retries}/{max_retries})")
            time.sleep(final_delay)

2. 봇 프로세스 자가 치유(Self-Healing) 쉘 스크립트 구축

파이썬 내부에서 아무리 에러를 방어해도, 메모리 부족(OOM)이나 OS 수준의 강제 종료로 봇 자체가 완전히 꺼지는 것까지는 방어할 수 없습니다.

가상 서버의 메모리(512MB RAM) 상황을 고려하여, 무거운 Node.js 기반의 프로세스 관리자(PM2) 대신 리눅스 표준 크론탭(Crontab)과 가벼운 쉘 스크립트의 조합으로 봇의 생사 여부를 1분마다 모니터링하고 자동으로 깨워내는 시스템을 조립합니다.

🛠️ 1 단계: 자가 치유 쉘 스크립트 작성 (check_bot.sh)

서버의 /home/ubuntu/bot/ 경로에 아래 내용으로 check_bot.sh 파일을 생성합니다.

#!/bin/bash

# [CRITICAL] 크론탭은 아주 극소량의 시스템 환경변수만 물고 작동합니다.
# 실행 파일 경로(PATH)를 명시적으로 주입하지 않으면 python3 명령어를 찾지 못할 수 있습니다.
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# 자동매매 봇 실행 스크립트의 경로 및 프로세스 명칭 정의
BOT_DIR="/home/ubuntu/bot"
PROCESS_NAME="main.py"
LOG_FILE="$BOT_DIR/realtime_trade.log"

cd "$BOT_DIR" || exit

# [TIP] 만약 가상환경(venv) 내부에서 패키지를 설치해 구동하는 구조라면,
# 아래와 같이 가상환경 폴더의 인터프리터 경로를 직접 타겟팅해야 모듈 누락 에러가 나지 않습니다.
# PYTHON_EXEC="$BOT_DIR/venv/bin/python3"
PYTHON_EXEC="python3"

# 프로세스가 가동 중인지 검사
# grep -v grep을 통해 grep 명령 자체의 프로세스는 검색 결과에서 제외시킵니다.
if ! ps aux | grep "$PROCESS_NAME" | grep -v grep > /dev/null
then
    # 프로세스가 죽어 있다면
    echo "[$(date)] 자동매매 봇이 중단된 상태입니다. 재시작을 시도합니다." >> "$BOT_DIR/self_healing.log"
    
    # nohup을 사용해 백그라운드로 봇 가동
    nohup $PYTHON_EXEC -u "$PROCESS_NAME" > "$LOG_FILE" 2>&1 &
    
    echo "[$(date)] 봇이 성공적으로 재구동되었습니다." >> "$BOT_DIR/self_healing.log"
else
    # 프로세스가 잘 돌고 있다면 아무것도 하지 않음
    exit 0
fi

스크립트 작성이 완료되면 터미널에서 실행 권한을 부여합니다.

chmod +x /home/ubuntu/bot/check_bot.sh

🛠️ 2 단계: 크론탭(Crontab)에 1분 주기 감시 등록

리눅스 스케줄러인 크론탭을 열어 1분마다 위 스크립트가 알아서 실행되도록 설정합니다.

# 크론탭 설정 편집기 실행
crontab -e

편집기 최하단에 아래 설정을 입력하고 저장합니다.

# 1분마다 check_bot.sh를 실행하여 봇의 생존 상태를 확인하고, 죽었다면 예토전생시킴
* * * * * /home/ubuntu/bot/check_bot.sh > /dev/null 2>&1

3. 실전 예외 방어 설계: CCXT 예외 핸들링 구조 표준

메인 매매 루프 내부에서 개별 예외를 계층적으로 묶어 봇 전체가 폭사하는 일을 원천 차단하는 예외 블록 설계의 표준 패턴입니다.

import sys

def main_trading_loop(exchange, db):
    while True:
        try:
            # 1. 1분마다 가격 분석 및 매매 신호 연산
            # call_api_with_backoff 함수를 사용해 안전하게 가격 로드
            ticker = call_api_with_backoff(exchange.fetch_ticker, 'BTC/USDT')
            
            # 매매 전략 판단 및 주문 접수 로직 실행...
            # (생략)
            
            time.sleep(60) # 1분 루프 주기
            
        except ccxt.InsufficientFunds as e:
            # 잔고 부족 에러: 더 이상 거래가 불가능하므로 관리자에게 경고 후 루프를 중단하거나 대기
            print(f"[🚨 심각 - 잔고 부족] 주문을 낼 예수금이 부족합니다: {e}")
            # 텔레그램 경고 메시지 송신 로직 연동
            time.sleep(300) # 경고가 도배되지 않도록 5분간 대기
            
        except ccxt.InvalidOrder as e:
            # 주문 요건 오류: 수수료나 최소 주문 수량 기준 미달 등
            print(f"[주문 오류] 유효하지 않은 주문 조건입니다: {e}")
            time.sleep(60)
            
        except Exception as e:
            # 그 외의 모든 런타임 에러(네트워크 단절, DB 락 충돌 등)
            print(f"[시스템 예외] 예상치 못한 오류 발생: {e}")
            # 시스템이 꼬여서 무한 에러가 날 수 있으므로 안전을 위해 10초 대기 후 재시도
            time.sleep(10)

⚠️ 리스크 경고: 무한 재시도의 덫과 스팸 늪

  • 무한 에러 루프의 텔레그램 스팸: 시스템 오류가 발생했을 때 텔레그램으로 즉각 에러 로그를 전송하게 코딩해 두면, 봇이 무한히 에러를 내며 재시도하는 과정에서 스마트폰이 1초에 수십 통의 텔레그램 알림 폭탄을 받게 됩니다. 결국 텔레그램 API 마저 호출 제한에 걸려 정작 중요한 킬스위치 신호조차 도달하지 못할 수 있습니다. 에러 알림은 동일 에러의 경우 최소 10분이나 1시간의 간격을 두고 누적 전송하도록 '스로틀링(Throttling)' 장치를 반드시 엮어야 합니다.
  • 의도치 않은 좀비 프로세스 복제: check_bot.sh 내부에서 프로세스명을 검색할 때, 파일명 검출 필터가 헐거우면(예: 파이썬 전체 프로세스를 검색하는 등) 봇이 이미 실행 중임에도 죽었다고 잘못 판정하여 동일한 봇이 백그라운드에 2개, 3개 겹쳐서 기동되는 좀비 프로세스 복제 참사가 일어날 수 있습니다. 이 경우 중복 주문이 들어가 자산이 순식간에 녹아내리게 되므로, ps aux 검사 조건에 봇 고유의 스크립트 파일명을 아주 정밀하게 명시해 주어야 합니다.

에필로그: 다음 단계는 '실전 배포 총정리 및 최종 가이드!'

에러 앞에서 유연하게 후퇴하는 지수 백오프와, 비명횡사한 봇을 60초 내에 강제로 일으켜 세우는 자가 치유 파수꾼(Crontab)이 완성되었습니다. 이제 진정한 무인 운용 인프라가 마침내 구축되었습니다.

  • 다음 편 예고 체크리스트:
    • [ ] My AI Stories 시즌 3 대단원의 마침표
    • [ ] 24시간 실전 클라우드 매매 자동화 시스템의 전체 아키텍처 다이어그램 총정리
    • [ ] 실전 배포 시 잊지 말아야 할 필수 10대 체크리스트
    • [ ] 향후 데이터 사이언스 및 머신러닝 모델 탑재를 위한 고도화 로드맵 제안

다음 편에서는 그동안 우리가 다져온 24시간 실전 자동매매 봇의 인프라와 로직을 한눈에 볼 수 있게 정립하고, 최종 배포를 완성하는 **'시즌 3 최종화: 24시간 실전 배포 총정리 및 무한 성장 로드맵 가이드'**로 찾아오겠습니다.

가장 튼튼한 시스템을 구축한 자가 결국 승리의 열매를 땁니다. 오늘 글이 흥미로우셨다면 구독과 따뜻한 공감 댓글 부탁드립니다. 감사합니다!

반응형