자동매매 시스템을 처음 띄웠을 때 가장 무서운 순간은 차트가 아니라 아침에 일어나서 로그를 열었을 때입니다. 새벽 3시에 TradingView가 웹훅을 보냈는데 Flask가 죽어있어서 신호 자체를 받지 못한 적이 있었습니다. 시장은 알아서 움직였고, 제 봇만 멈춰있었던 거죠. 그날 이후 저는 Flask API 안정성을 단순한 개발 문제가 아니라 운영 문제로 다루기 시작했습니다. 이번 칼럼은 그 과정에서 직접 부딪힌 함정과 해결책을 정리한 기록입니다.
Flask가 죽는 방식은 생각보다 다양합니다 — 문제 유형부터 분류했습니다
처음에는 단순히 ‘프로세스가 크래시났다’고만 생각했습니다. 그런데 실제로 로그를 뜯어보니 죽는 방식이 제각각이었습니다. 크게 세 가지 유형으로 나눠졌습니다. 첫째는 OOM(Out of Memory)입니다. Jesse 프레임워크가 백테스트 루틴을 돌릴 때 메모리를 많이 씁니다. Cafe24 VPS의 기본 메모리가 빡빡하면, Flask 프로세스가 커널에 의해 조용히 kill됩니다. dmesg 로그에서 ‘oom-kill’ 키워드로 검색하면 확인할 수 있습니다. 둘째는 unhandled exception입니다. 웹훅 JSON 파싱 중 예상치 못한 필드가 들어오면 Flask 워커 전체가 내려가는 경우가 있었습니다. try-except 블록을 라우트 단위가 아니라 파서 함수 단위로 분리하지 않으면 이 문제가 반복됩니다. 셋째는 포트 충돌입니다. 제가 수동으로 재시작하면서 이전 프로세스가 완전히 종료되기 전에 새 프로세스를 띄우면, 5000번 포트를 두 개가 물고 있다가 둘 다 비정상 종료되는 상황이 발생했습니다. lsof -i :5000 명령어로 확인하는 습관을 만들었습니다. 이 세 가지 유형을 구분한 뒤에야 각각 다른 해결책을 적용할 수 있었습니다. 문제를 하나의 덩어리로 보면 해결책도 뭉뚱그려집니다.
systemd + Gunicorn + Nginx — 이 세 층이 있어야 비로소 안정이 됩니다
Flask를 python app.py로 직접 실행하는 건 개발용입니다. 운영 환경에서는 최소 세 층이 필요하다는 걸 시행착오로 배웠습니다. 첫 번째 층은 Gunicorn입니다. Flask 내장 서버는 단일 스레드라 웹훅이 동시에 몇 개만 들어와도 큐가 밀립니다. gunicorn -w 2 -b 127.0.0.1:5000 app:app 처럼 워커 수를 2개로 설정하면 동시 처리가 가능해집니다. VPS 메모리가 작으면 워커 수를 늘리는 게 오히려 OOM을 유발하니 1~2개가 적당합니다. 두 번째 층은 systemd 서비스 등록입니다. /etc/systemd/system/flask-webhook.service 파일을 만들고 Restart=always, RestartSec=5 옵션을 넣으면, 프로세스가 죽었을 때 5초 후 자동으로 재시작됩니다. 저는 여기에 MemoryMax=400M 옵션도 추가해서 Flask 프로세스가 메모리를 과도하게 먹으면 systemd가 먼저 정리하도록 설정했습니다. 세 번째 층은 Nginx 리버스 프록시입니다. TradingView 웹훅은 외부 IP로 직접 쏩니다. Nginx를 앞에 세우면 80/443 포트로 받아서 내부 5000포트로 전달합니다. 이렇게 하면 Flask가 죽어도 Nginx는 살아있기 때문에 503 응답을 TradingView에 돌려줄 수 있고, 재시작 후 자동으로 연결이 복구됩니다. 이 세 층을 붙이기 전과 후의 가동 시간 차이는 체감상 완전히 달랐습니다.
모니터링 없는 안정성은 착각입니다 — 제가 쓰는 저비용 알림 구조
systemd가 자동 재시작을 해준다고 해도, 재시작이 반복되고 있다는 사실 자체를 모르면 근본 원인을 못 잡습니다. 저는 별도의 유료 모니터링 서비스 없이 세 가지 방법을 조합해서 씁니다. 첫 번째는 systemd 재시작 카운터 체크 스크립트입니다. 매 시간 cron으로 systemctl show flask-webhook –property=NRestarts 값을 읽어서 이전 값보다 늘었으면 텔레그램 봇으로 알림을 보냅니다. 파이썬 20줄짜리 스크립트로 구현했습니다. 두 번째는 헬스체크 엔드포인트입니다. Flask에 /health 라우트를 만들고 GET 요청에 200 OK를 돌려주도록 합니다. 외부 무료 서비스인 UptimeRobot에서 5분마다 이 주소를 체크하고, 응답 없으면 이메일로 알립니다. 무료 플랜으로 충분합니다. 세 번째는 웹훅 수신 로그 파일 크기 체크입니다. Flask가 정상 동작하면 웹훅 수신 시마다 로그가 쌓입니다. 하루에 로그가 전혀 늘지 않으면 뭔가 이상한 겁니다. cron으로 자정에 로그 사이즈를 전날과 비교해서 변화가 없으면 알림을 보냅니다. 이 세 가지를 같이 쓰면 ‘죽었다는 사실’뿐 아니라 ‘얼마나 자주 죽는지’까지 파악할 수 있습니다. 저는 이 구조를 만든 뒤 실제로 재시작이 하루에 수차례 반복되던 근본 원인을 찾아서 잡을 수 있었습니다.
💬 운영자 한마디
저는 Flask를 처음 올릴 때 ‘python app.py 하고 화면 닫으면 되겠지’라고 생각했던 사람입니다. 그 시절 제 봇은 하루에도 몇 번씩 조용히 멈췄습니다. 지금은 systemd 로그를 아침에 확인하는 게 습관이 됐고, 재시작 횟수가 0이면 그날 하루가 안심이 됩니다. 안정성은 한 번에 완성되는 게 아니라 실패 로그를 읽으면서 쌓이는 것 같습니다.
— J_River · autoprofit 운영자
✅ 이번 주 체크리스트
- dmesg | grep oom-kill 로 OOM 발생 이력 먼저 확인하기
- python app.py 직접 실행 대신 Gunicorn으로 전환하기 (워커 1~2개)
- /etc/systemd/system/ 에 서비스 파일 등록 후 Restart=always 설정
- systemd MemoryMax 옵션으로 프로세스 메모리 상한 설정하기
- Flask에 /health 엔드포인트 추가 후 UptimeRobot 무료 모니터링 연결
- systemd NRestarts 값을 cron으로 주기 체크 → 텔레그램 알림 스크립트 작성
- Nginx 리버스 프록시 앞에 세워서 Flask 재시작 중에도 503 응답 보장하기
Flask API 하나가 안정적으로 돌아가는 것, 생각보다 손이 많이 갑니다. 하지만 이 구조를 한 번 만들어두면 다른 봇을 추가할 때도 같은 틀을 그대로 씁니다. 이번 주 점검 항목에 VPS 프로세스 상태 한 번 확인해보시길 권합니다.
※ 본 콘텐츠는 운영자 개인의 경험과 연구를 바탕으로 한 정보 제공 목적이며, 특정 종목에 대한 투자 권유나 매매 시그널이 아닙니다. 암호화폐 및 자동매매는 원금 손실 가능성이 있는 고위험 활동이며, 모든 투자 판단과 그 결과에 대한 책임은 투자자 본인에게 있습니다.