텔레그램 봇

타임아웃, 예외 처리와 무중단 복구

NetworkError, TimedOut, 429 RetryAfter, 봇 차단 처리 및 전역 에러 핸들러와 실시간 웹훅 알림 완벽 가이드.

업데이트: 2026-10-02

봇을 24시간 상시 운영할 때 프로세스가 중단되는 가장 흔한 원인은 문법 버그가 아니라, 외부 네트워크의 일시적 순단(순시 단절), 텔레그램 API 타임아웃, 사용자가 봇을 차단했을 때 발생하는 예외를 코드에서 적절히 처리하지 못했기 때문입니다.

처리되지 않은 예외 하나로 봇 전체가 크래시되지 않도록 방어하는 프로덕션급 에러 핸들링 기법을 상세히 알아봅니다.


텔레그램 봇 5대 핵심 예외 유형과 대응 전략

예외 클래스발생 원인기본 동작 결과권장 대응 전략
telegram.error.TimedOut텔레그램 서버 응답 지연 (네트워크 지연)즉시 프로세스 크래시 위험일시적 현상이므로 즉시 무시하고 다음 폴링 루프 진행
telegram.error.NetworkError호스트 인터넷 순시 단절, DNS 룩업 지연소켓 강제 단절지수 백오프(1초 ➔ 2초 ➔ 4초) 기반 자동 재접속
telegram.error.RetryAfter429 Too Many Requests 초당 발송 한도 초과거래소/텔레그램 일시 차단e.retry_after 초만큼 정확히 대기 후 재전송
telegram.error.Forbidden사용자가 봇을 차단했거나 방에서 내보냄전송 실패 크래시DB에서 해당 유저의 활성 플래그를 is_active=0으로 변경 후 제외
telegram.error.BadRequest수정할 내용이 이전과 동일하거나 텍스트 누락파라미터 에러단순 로깅 후 정상 패스 (치명적 오류 아님)

전역 에러 핸들러 구축

python-telegram-bot v21에서는 모든 핸들러에서 발생하는 예외를 한곳으로 모아 처리하는 add_error_handler 기능을 제공합니다.

어떤 예외가 터져도 봇 프로세스가 죽지 않으며, 에러 트레이스백 전문을 관리자의 개인 텔레그램 대화방으로 즉각 보고합니다.

error_handlers.py
import html
import json
import logging
import traceback
import os
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import ContextTypes
 
logger = logging.getLogger("ErrorHandler")
ADMIN_CHAT_ID = os.environ.get("TELEGRAM_ADMIN_CHAT_ID")
 
async def global_error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
    """모든 처리되지 않은 예외를 가로채어 로깅하고 관리자에게 보고"""
    logger.error("요청 처리 중 치명적 예외 발생:", exc_info=context.error)
 
    # 1. 상세 트레이스백 텍스트 추출
    tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
    tb_string = "".join(tb_list)
 
    # 2. 업데이트 정보 요약
    update_str = update.to_dict() if isinstance(update, Update) else str(update)
    
    # 3. 관리자에게 보낼 긴급 경보 메시지 조립
    message = (
        "🚨 <b>봇 시스템 예외 경보 발생!</b>\n\n"
        f"<b>에러 내용:</b> <code>{html.escape(str(context.error))}</code>\n\n"
        f"<b>트레이스백:</b>\n<pre>{html.escape(tb_string[-2000:])}</pre>"
    )
 
    # 관리자 채팅방으로 에러 알림 발송 (관리자 방 발송 자체가 실패해도 봇은 유지)
    if ADMIN_CHAT_ID:
        try:
            await context.bot.send_message(
                chat_id=int(ADMIN_CHAT_ID),
                text=message,
                parse_mode=ParseMode.HTML
            )
        except Exception as e:
            logger.error(f"관리자 알림 전송 실패: {e}")
 
    # 4. 사용자에게 친절한 안내 메시지 전송 (옵션)
    if isinstance(update, Update) and update.effective_message:
        try:
            await update.effective_message.reply_text(
                "⚠️ 요청을 처리하는 도중 일시적인 시스템 오류가 발생했습니다. 관리자에게 자동 보고되었습니다."
            )
        except Exception:
            pass

지수 백오프 재연결 데코레이터

외부 API 호출이나 네트워크 전송 시 일시적인 타임아웃에 대처하는 자동 재시도 데코레이터입니다.

retry.py
import asyncio
import functools
import logging
from telegram.error import TimedOut, NetworkError, RetryAfter
 
logger = logging.getLogger("Retry")
 
def with_retry(max_retries: int = 3, base_delay: float = 1.0):
    """네트워크 타임아웃 발생 시 지수 백오프로 재시도하는 데코레이터"""
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            retries = 0
            while True:
                try:
                    return await func(*args, **kwargs)
                except RetryAfter as e:
                    # 429 속도 제한: 텔레그램이 지시한 초만큼 대기
                    logger.warning(f"429 속도 제한. {e.retry_after}초 후 재시도합니다.")
                    await asyncio.sleep(e.retry_after + 0.5)
                except (TimedOut, NetworkError) as e:
                    retries += 1
                    if retries > max_retries:
                        logger.error(f"최대 재시도 횟수({max_retries}회) 초과: {e}")
                        raise
                    delay = base_delay * (2 ** (retries - 1))
                    logger.warning(f"네트워크 오류 ({e}). {delay}초 후 재시도 ({retries}/{max_retries})...")
                    await asyncio.sleep(delay)
        return wrapper
    return decorator
 
# 사용 예시:
# @with_retry(max_retries=3)
# async def send_report(bot, chat_id, text):
#     await bot.send_message(chat_id=chat_id, text=text)

사용자의 봇 차단(Forbidden) 안전 처리

대량 알림을 보낼 때 특정 사용자가 봇을 차단했으면 telegram.error.Forbidden: Bot was blocked by the user 에러가 발생합니다.

이를 제대로 예외 처리하지 않으면 전체 발송 루프가 중간에 멈추어 나머지 사용자에게 알림이 가지 않는 사고가 발생합니다.

safe_sender.py
import logging
from telegram.error import Forbidden, BadRequest
 
logger = logging.getLogger("SafeSender")
 
async def send_notification_to_user(bot, user_id: int, text: str, db):
    try:
        await bot.send_message(chat_id=user_id, text=text)
        return True
    except Forbidden:
        # 사용자가 봇을 차단함: DB에서 알림 수신 거부 처리하여 다음 루프부터 제외
        logger.info(f"유저 [{user_id}]가 봇을 차단했습니다. 수신 비활성화 처리합니다.")
        await db.execute("UPDATE users SET is_active = 0 WHERE user_id = ?", (user_id,))
        return False
    except BadRequest as e:
        if "Chat not found" in str(e):
            logger.warning(f"유저 [{user_id}] 대화방이 존재하지 않습니다.")
            await db.execute("UPDATE users SET is_active = 0 WHERE user_id = ?", (user_id,))
        else:
            logger.error(f"유저 [{user_id}] 전송 실패: BadRequest - {e}")
        return False

텔레그램 봇 완성형 메인 진입점 연동

main.py
import os
import logging
from telegram.ext import Application, CommandHandler
from error_handlers import global_error_handler
 
logging.basicConfig(level=logging.INFO)
 
async def test_error(update, context):
    # 의도적인 0 나누기 에러 테스트
    result = 1 / 0
 
def main():
    token = os.environ.get("TELEGRAM_BOT_TOKEN")
    app = Application.builder().token(token).build()
 
    # 핸들러 및 전역 에러 핸들러 바인딩
    app.add_handler(CommandHandler("crash_test", test_error))
    app.add_error_handler(global_error_handler)
 
    app.run_polling(drop_pending_updates=True)
 
if __name__ == "__main__":
    main()

다음 추천 문서