TuBrief
구독 채널
비디오
커뮤니티

밤새 돌려둔 AI 자동화 봇이 아침마다 죽어있는 진짜 이유

TuBrief 편집팀
2026년 7월 15일
0
컴퓨터/소프트웨어

원본 영상을 바탕으로 AI의 도움을 받아 작성했습니다. 원본 영상이 기준입니다.

한국어EnglishEspañol中文العربيةहिन्दीDeutschFrançaisРусскийBahasa Indonesia日本語Português

관련 영상

Claude를 나만의 개인 비서로 만드는 방법 (완벽한 시스템)22:25

Claude를 나만의 개인 비서로 만드는 방법 (완벽한 시스템)

Chase AI

커뮤니티의 다른 글

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

2026년 9월 13일

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

2026년 9월 13일

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

2026년 9월 13일

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

2026년 9월 13일

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

2026년 9월 12일

Apple Won the AI Race

2026년 9월 12일

댓글 (0)

Log in to leave a comment

아직 작성된 글이 없습니다

© 2026 . All rights reserved.

TuBrief
구독 채널
비디오
커뮤니티
로그인

밤새 돌려둔 AI 자동화 봇이 아침마다 죽어있는 진짜 이유

유튜브에서 보여주는 AI 자동화는 쉽고 화려합니다. 로컬 컴퓨터에서 코드를 실행해 한번 성공하면 다 끝난 것 같습니다. 하지만 그건 절반만 온 겁니다.

이 스크립트를 클라우드 서버에 올리는 순간부터 진짜 문제가 시작됩니다. 어제까지 잘 돌던 봇이 아침에 일어나 보면 죽어있습니다. 에러 메시지도 없이 조용히 멈춰있거나, 최악의 경우 무한 루프를 돌며 API 비용만 수백 달러를 청구하기도 합니다.

로컬과 서버의 미세한 환경 차이, 예외 처리 없는 API 호출, 모니터링 부재는 주니어 개발자와 1인 지식 창업자가 자동화를 시도할 때 반드시 마주치는 장벽입니다. 24시간 안정적으로 돌아가는 자동화 시스템을 구축하기 위해 지금 당장 뜯어고쳐야 할 3가지 뼈대를 정리했습니다.


의존성 관리의 늪: Poetry로 환경 고정하기

파이썬 기반 자동화 시스템이 서버에서 멈추는 첫 번째 원인은 라이브러리 버전 불일치입니다. 많은 사람들이 pip freeze > requirements.txt 방식을 사용합니다. 하지만 이 방식은 설치 시점의 환경이나 운영체제에 따라 하위 패키지 버전이 제멋대로 바뀌는 치명적인 결함이 있습니다. 내 컴퓨터에서는 잘 되는데 서버에서는 패키지 충돌로 실행조차 안 되는 현상이 이래서 일어납니다.

실제로 미국의 글로벌 인사관리 플랫폼 기업 리플링(Rippling)의 엔지니어링 팀은 패키지 의존성 지옥을 해결하기 위해 기존 패키지 관리 도구를 과감히 버리고 Rust 기반의 패키지 관리자 uv를 도입했습니다. 빌드와 라이브러리 설치 시간을 기존 10분에서 1분 미만으로 줄여 생산성을 높인 사례입니다.

자동화가 죽지 않게 하려면 가상환경을 완전히 격리하고 하위 패키지 버전까지 철저하게 잠가야 합니다. 파이썬 표준 규격인 pyproject.toml을 지원하는 포에트리(Poetry)를 사용하면 이 문제를 해결할 수 있습니다.

먼저 터미널을 열고 Poetry를 설치한 뒤 프로젝트 폴더를 만듭니다.

curl -sSL https://install.python-poetry.org | python3 -
poetry new ai_automation_project
cd ai_automation_project

그 다음 가상환경이 시스템 전역 폴더가 아니라 프로젝트 폴더 내부(-project)에 생성되도록 설정을 변경합니다. 이렇게 해야 폴더 통째로 서버에 올렸을 때 꼬이지 않습니다.

poetry config virtualenvs.in-project true --local

이제 필요한 외부 라이브러리와 개발용 테스트 도구를 추가합니다. 고유 해시값이 기록된 poetry.lock 파일이 만들어지며 환경이 완전히 고정됩니다.

poetry add anthropic python-dotenv sqlite3
poetry add --group dev pytest pytest-mock
poetry install --sync

프로젝트 루트 폴더 안에 .venv 디렉터리가 생성되었다면 성공입니다. 로컬과 서버의 라이브러리 싱크를 일치시키는 것만으로도 배포 시 오작동으로 버리는 시간을 절반 이상 줄일 수 있습니다.


배포 전 필수 단계: Pytest로 API 응답 예외 테스트하기

로컬에서 수동으로 스크립트를 실행해 성공한 것은 검증이 아닙니다. 클라우드 서버에 올라간 봇은 네트워크 지연, API 서버의 일시적인 먹통, 빈 문자열 반환 같은 온갖 에러 환경에 무방비로 노출됩니다. 일시적인 통신 오류가 발생했을 때 적절한 예외 처리가 되어 있지 않다면 봇은 그 자리에서 죽어버리거나 깨진 데이터를 데이터베이스에 그대로 밀어 넣습니다.

유료 API 호출 비용을 아끼면서 이런 예외 상황을 완벽하게 검증하려면 실제 API 서버로 요청을 보내는 대신 모의 객체(Mock)를 활용하는 유닛 테스트를 작성해야 합니다.

먼저 외부 API 호출 클래스를 주입식 패턴으로 격리하여 app_bot.py를 작성합니다.

# app_bot.py
import time
import anthropic
from anthropic import Anthropic

class ClaudeAutomationBot:
    def __init__(self, client: Anthropic = None):
        self.client = client or Anthropic()

    def process_task(self, prompt: str, timeout_limit: float = 5.0) -> dict:
        if not prompt.strip():
            raise ValueError("입력된 프롬프트가 유효하지 않습니다.")

        start_time = time.perf_counter()
        try:
            response = self.client.with_options(timeout=timeout_limit).messages.create(
                model="claude-3-5-sonnet-20241022",
                max_tokens=1000,
                messages=[{"role": "user", "content": prompt}]
            )
            elapsed_time = time.perf_counter() - start_time

            if not response.content or not response.content[0].text:
                raise ValueError("API 응답 내 텍스트 데이터 블록이 부재합니다.")

            return {
                "status": "success",
                "text": response.content[0].text,
                "latency": elapsed_time
            }
        except anthropic.APITransitError:
            return {"status": "error", "reason": "timeout", "latency": time.perf_counter() - start_time}
        except Exception as e:
            return {"status": "error", "reason": str(e), "latency": time.perf_counter() - start_time}

이제 동일한 디렉터리에 가짜 응답을 주입해 정상 작동과 비정상 데이터를 강제로 시뮬레이션하는 test_app_bot.py를 만듭니다.

# test_app_bot.py
import pytest
from app_bot import ClaudeAutomationBot
from anthropic import Anthropic
from anthropic.types import Message, Usage
from anthropic.types.content_block import ContentBlock

def test_api_response_data_presence_success(mocker):
    mock_client = mocker.MagicMock(spec=Anthropic)
    mock_message = mocker.MagicMock(spec=Message)
    mock_content = mocker.MagicMock(spec=ContentBlock)
    mock_content.type = "text"
    mock_content.text = "가공 및 정합성이 완전한 데이터입니다."

    mock_message.content = [mock_content]
    mock_message.usage = Usage(input_tokens=15, output_tokens=25)
    mock_client.messages.create.return_value = mock_message

    bot = ClaudeAutomationBot(client=mock_client)
    result = bot.process_task("정상 요약 테스트")

    assert result["status"] == "success"
    assert result["text"] == "가공 및 정합성이 완전한 데이터입니다."
    assert result["latency"] > 0

def test_api_response_empty_data_failure(mocker):
    mock_client = mocker.MagicMock(spec=Anthropic)
    mock_message = mocker.MagicMock(spec=Message)
    mock_content = mocker.MagicMock(spec=ContentBlock)
    mock_content.type = "text"
    mock_content.text = "" 

    mock_message.content = [mock_content]
    mock_client.messages.create.return_value = mock_message

    bot = ClaudeAutomationBot(client=mock_client)
    result = bot.process_task("비정상 입력 테스트")

    assert result["status"] == "error"
    assert "텍스트 데이터 블록이 부재합니다" in result["reason"]

마지막으로 GitHub 저장소의 .github/workflows/python-package.yml 경로에 코드가 올라갈 때마다 이 테스트를 자동 실행하는 CI 파이프라인을 구축합니다.

name: Python Automation Bot Test and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: 저장소 소스 체크아웃
      uses: actions/checkout@v4

    - name: 파이썬 3.12 환경 핀 설정
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: Poetry 종속성 설치 및 환경 설정
      uses: abatilo/actions-poetry@v4

    - name: 프로젝트 종속성 설치
      run: |
        poetry config virtualenvs.create true --local
        poetry config virtualenvs.in-project true --local
        poetry install --no-interaction

    - name: 무결성 검증용 단위 테스트 구동
      run: |
        poetry run pytest -v

레일웨이(Railway)나 버셀(Vercel) 같은 클라우드 환경에서 이 테스트를 연동하면 테스트가 하나라도 깨졌을 때 배포 자체를 원천 차단합니다. 사소한 오타나 잘못된 예외 처리 때문에 운영 서버 전체가 마비되는 참사를 막아줍니다.


과금 폭탄 방지: 로컬 DB와 연동한 실시간 킬 스위치

자동화 인프라를 운영할 때 직면하는 가장 무서운 현실은 비용입니다. 루프 종결 조건이 꼬여 무한 루프에 빠지거나 프롬프트 캐싱 설정이 풀린 채로 무거운 컨텍스트가 계속 전송되면 자고 일어났을 때 수백 달러의 청구서를 받게 됩니다.

이를 막으려면 API 호출 전송 전에 로컬 DB를 확인하여 당일 누적 지출액을 계산하고, 설정한 하루 한계선을 초과하면 시스템을 강제로 멈추는 하드웨어 수준의 킬 스위치가 있어야 합니다.

먼저 SQLite를 사용해 API 호출 사용량을 기록하는 database_manager.py를 만듭니다.

# database_manager.py
import sqlite3
import time

class DBUsageLogger:
    def __init__(self, db_path: str = "monitoring_usage.db"):
        self.db_path = db_path
        self._setup_table()

    def _setup_table(self):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS api_metrics (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    timestamp REAL,
                    model TEXT,
                    input_tokens INTEGER,
                    cache_read_tokens INTEGER,
                    output_tokens INTEGER,
                    cost_usd REAL
                )
            """)
            conn.commit()

    def get_today_accumulated_spend(self) -> float:
        utc_today_start = time.time() - (time.time() % 86400)
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.cursor()
            cursor.execute(
                "SELECT SUM(cost_usd) FROM api_metrics WHERE timestamp >= ?",
                (utc_today_start,)
            )
            result = cursor.fetchone()
            return result[0] if result[0] is not None else 0.0

    def insert_transaction(self, model: str, input_t: int, cache_r_t: int, output_t: int, cost: float):
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                INSERT INTO api_metrics 
                (timestamp, model, input_tokens, cache_read_tokens, output_tokens, cost_usd) 
                VALUES (?, ?, ?, ?, ?, ?)
            """, (time.time(), model, input_t, cache_r_t, output_t, cost))
            conn.commit()

그 다음 API 호출 전 예산을 체크하고 요율표를 기준으로 비용을 실시간 정산하는 미들웨어 kill_switch_middleware.py를 작성합니다. 요율은 앤트로픽 공식 단가 기준(100만 토큰당 입력 3달러, 아웃풋 15달러, 프롬프트 캐시 읽기 0.3달러)을 적용합니다.

# kill_switch_middleware.py
from database_manager import DBUsageLogger

class APIBudgetKillSwitch:
    def __init__(self, logger_db: DBUsageLogger, max_daily_budget_usd: float = 5.00):
        self.db = logger_db
        self.limit = max_daily_budget_usd

    def check_pre_flight_safety(self):
        today_spend = self.db.get_today_accumulated_spend()
        if today_spend >= self.limit:
            raise PermissionError(
                f"[SECURITY BLOCK] 일일 API 사용 허용 한도 초과! "
                f"설정 예산: ${self.limit:.2f}, 현재 누적액: ${today_spend:.4f}. "
                f"안전을 위해 프로세스를 강제 중지합니다."
            )

    def calculate_and_save_cost(self, model: str, input_t: int, cache_r_t: int, output_t: int) -> float:
        pricing_chart = {
            "claude-3-5-sonnet-20241022": {"input": 3.0, "cache_read": 0.3, "output": 15.0},
            "claude-3-5-haiku-20241022": {"input": 1.0, "cache_read": 0.1, "output": 5.0},
        }
        prices = pricing_chart.get(model, {"input": 3.0, "cache_read": 0.3, "output": 15.0})
        fresh_input = input_t - cache_r_t

        cost = (
            (fresh_input * (prices["input"] / 1000000.0)) +
            (cache_r_t * (prices["cache_read"] / 1000000.0)) +
            (output_t * (prices["output"] / 1000000.0))
        )

        self.db.insert_transaction(model, input_t, cache_r_t, output_t, cost)
        return cost

봇이 실행될 때 루프 가장 앞에서 check_pre_flight_safety()를 호출해 안전을 점검하고, API 호출이 끝나면 사용된 토큰 수와 비용을 실시간으로 데이터베이스에 저장합니다.

하루에 5달러가 넘게 쓰이면 프로그램이 스스로 통신을 차단하고 뻗어버립니다. 오작동으로 지갑이 거덜 나는 것보다는 스크립트가 알아서 종료되는 편이 백번 안전합니다.


사후 대처: 에러 발생 즉시 상세 트레이스백 알림 받기

클라우드에 안전하게 배포를 마쳤어도 예상치 못한 외부 서버 오류 등으로 멈출 수 있습니다. 주니어 개발자나 1인 창업자가 클라우드 대시보드의 실시간 로그 창을 하루 종일 쳐다보는 것은 시간 낭비입니다.

에러가 나서 프로그램이 멈췄을 때, 정확히 몇 번째 라인에서 어떤 변수 값 때문에 터졌는지 텔레그램으로 즉시 알림을 쏴주는 체계가 필요합니다.

# notification_alert.py
import html
import json
import logging
import traceback
import requests
from datetime import datetime

logger = logging.getLogger("ProdAppLogger")
logger.setLevel(logging.INFO)
log_format = logging.Formatter(
    "[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d]: %(message)s"
)
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_format)
logger.addHandler(console_handler)

class TelegramEmergencyNotifier:
    def __init__(self, bot_token: str, target_chat_id: str):
        self.token = bot_token
        self.chat_id = target_chat_id
        self.api_url = f"https://api.telegram.org/bot{self.token}/sendMessage"

    def dispatch_error_log(self, error: Exception, context_data: dict = None):
        trace_back = traceback.format_exception(type(error), error, error.__traceback__)
        joined_trace = "".join(trace_back)

        safe_trace = html.escape(joined_trace)
        safe_context = html.escape(json.dumps(context_data or {}, indent=2, ensure_ascii=False))

        if len(safe_trace) > 3000:
            safe_trace = safe_trace[:3000] + "\n[...이하 로그 생략...]"

        alert_body = (
            f"⚠️ <b>[자동화 봇 운영 긴급 에러 경보]</b>\n"
            f"📅 <b>발생 시각 (KST):</b> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
            f"💻 <b>발생 모듈:</b> AI 자동화 파이프라인\n\n"
            f"⚙️ <b>에러 전후 Context 상태 데이터:</b>\n"
            f"<pre>{safe_context}</pre>\n\n"
            f"📁 <b>상세 역추적 트레이스백 (Traceback):</b>\n"
            f"<pre>{safe_trace}</pre>"
        )

        payload = {
            "chat_id": self.chat_id,
            "text": alert_body,
            "parse_mode": "HTML"
        }

        try:
            response = requests.post(self.api_url, json=payload, timeout=10)
            if response.status_code != 200:
                logger.error(f"Telegram Alert failed. Status: {response.status_code}")
        except requests.exceptions.RequestException as net_err:
            logger.error(f"Alert delivery network crash. Details: {str(net_err)}")

메인 프로세스를 감싸는 try-except 최외곽 블록에 이 알림 모듈을 연결해둡니다. 오류가 터졌을 때 에러 트레이스백을 기반으로 소스코드 파일 위치와 예외 메시지가 모바일로 실시간 전송됩니다.

서버 터미널에 접속해 낯선 영어 로그를 한참 뒤지지 않아도 알림 창만 보고 바로 현장에서 수정 작업을 들어갈 수 있어 대응 시간이 빨라집니다.


지속 가능한 자동화 인프라

한두 번 실행에 성공하는 코드는 누구나 만듭니다. 하지만 그것을 서버 위에서 고장 없이 365일 돌리는 일은 전혀 다른 문제입니다.

Poetry로 전역 가상환경 오염을 차단하고 의존성을 맞추는 것부터 시작하십시오. 로컬 DB에 요율표를 내장해 지갑을 방어하고, 에러가 나면 텔레그램으로 바로 상황을 파악해야 합니다. 이런 인프라 수준의 장치들이 겹겹이 쌓여야 비로소 AI 자동화 봇이 비즈니스를 돕는 든든한 일꾼으로 활약합니다.