타임아웃, 예외 처리와 무중단 복구
업데이트: 2026-10-02
봇을 24시간 상시 운영할 때 프로세스가 중단되는 가장 흔한 원인은 문법 버그가 아니라, 외부 네트워크의 일시적 순단(순시 단절), 텔레그램 API 타임아웃, 사용자가 봇을 차단했을 때 발생하는 예외를 코드에서 적절히 처리하지 못했기 때문입니다.
처리되지 않은 예외 하나로 봇 전체가 크래시되지 않도록 방어하는 프로덕션급 에러 핸들링 기법을 상세히 알아봅니다.
텔레그램 봇 5대 핵심 예외 유형과 대응 전략
| 예외 클래스 | 발생 원인 | 기본 동작 결과 | 권장 대응 전략 |
|---|---|---|---|
telegram.error.TimedOut | 텔레그램 서버 응답 지연 (네트워크 지연) | 즉시 프로세스 크래시 위험 | 일시적 현상이므로 즉시 무시하고 다음 폴링 루프 진행 |
telegram.error.NetworkError | 호스트 인터넷 순시 단절, DNS 룩업 지연 | 소켓 강제 단절 | 지수 백오프(1초 ➔ 2초 ➔ 4초) 기반 자동 재접속 |
telegram.error.RetryAfter | 429 Too Many Requests 초당 발송 한도 초과 | 거래소/텔레그램 일시 차단 | e.retry_after 초만큼 정확히 대기 후 재전송 |
telegram.error.Forbidden | 사용자가 봇을 차단했거나 방에서 내보냄 | 전송 실패 크래시 | DB에서 해당 유저의 활성 플래그를 is_active=0으로 변경 후 제외 |
telegram.error.BadRequest | 수정할 내용이 이전과 동일하거나 텍스트 누락 | 파라미터 에러 | 단순 로깅 후 정상 패스 (치명적 오류 아님) |
전역 에러 핸들러 구축
python-telegram-bot v21에서는 모든 핸들러에서 발생하는 예외를 한곳으로 모아 처리하는 add_error_handler 기능을 제공합니다.
어떤 예외가 터져도 봇 프로세스가 죽지 않으며, 에러 트레이스백 전문을 관리자의 개인 텔레그램 대화방으로 즉각 보고합니다.
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 호출이나 네트워크 전송 시 일시적인 타임아웃에 대처하는 자동 재시도 데코레이터입니다.
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 에러가 발생합니다.
이를 제대로 예외 처리하지 않으면 전체 발송 루프가 중간에 멈추어 나머지 사용자에게 알림이 가지 않는 사고가 발생합니다.
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텔레그램 봇 완성형 메인 진입점 연동
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()