파이썬 업무 자동화#13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기
목차11개 항목
안녕하세요, Mojangcam입니다 😎
아침에 구글 시트를 열었는데 어제 숫자가 그대로예요. 자동화가 아예 실행되지 않은 건지, 데이터를 읽다가 멈춘 건지, 마지막 저장만 실패한 건지 알 수가 없죠.
지난 구글 시트 연동 편에서는 파이썬의 실행 결과를 팀원들과 공유했어요. 이번에는 그 작업을 감싸는 로그·재시도·실패 알림을 붙여볼게요. 완성하면 일시적인 연결 오류는 정해진 횟수만 다시 시도하고, 끝내 실패하면 기록과 종료 코드로 확인할 수 있어요.
먼저 알아둘 점이 있어요. 이 예제는 실제로 시작된 프로그램 안에서 발생한 오류를 다룹니다. PC가 꺼져 있거나 예약 작업이 시작조차 안 된 경우에는 이 코드도 실행되지 않아요. 그 상황은 별도의 실행 여부 감시가 필요합니다.
📌 오늘 만들 결과
흐름은 간단해요. 작업 시작 → 실행 기록 → 성공하면 종료 / 일시 오류면 최대 3회 시도 → 최종 실패 때만 알림입니다.
| 직접 실행할 모드 | 동작 | 종료 코드 |
|---|---|---|
| success | 한 번에 성공 | 0 |
| retry | 1초, 2초 기다린 뒤 세 번째에 성공 | 0 |
| fail | 세 번 모두 실패 | 1 |
| invalid | 잘못된 입력은 한 번만 실행하고 실패 | 1 |
외부 서버가 일부러 고장 나기를 기다릴 필요는 없어요. 샘플 작업에 오류를 넣어 네 가지 상황을 재현할 거예요. 기본 실행은 디스코드 메시지를 전송하지 않습니다. 마지막 단계에서 웹훅과 --notify 옵션을 함께 설정해야 실제 알림이 나가요.
📌 준비물과 폴더 구성
검증 환경은 Windows / Python 3.14.7 / requests 2.34.2 / python-dotenv 1.2.3입니다. logging, argparse, pathlib는 파이썬 기본 모듈이라 따로 설치하지 않아요.
아래 명령은 Windows PowerShell 기준이에요. 가상환경을 활성화하지 않고 실행 파일 경로를 직접 지정해, 다른 파이썬에 패키지를 설치하는 실수를 줄였어요.
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install requests==2.34.2 python-dotenv==1.2.3reliable-report/
├─ reliable_job.py
├─ .env
├─ .gitignore
└─ logs/ # 첫 실행 때 자동 생성
└─ report.log.env는 처음에는 다음 한 줄만 넣으면 돼요. 값이 비어 있어도 데모는 실행됩니다.
DISCORD_WEBHOOK_URL=.gitignore도 같이 만들어주세요.
.env
.venv/
logs/
__pycache__/📌 1단계: 화면과 파일에 같은 로그 남기기
print()만 사용하면 터미널 창을 닫은 뒤 기록을 놓치기 쉬워요. 이번에는 화면에 출력하는 StreamHandler와 파일에 보관하는 RotatingFileHandler를 함께 붙입니다.
file_handler = RotatingFileHandler(
log_dir / "report.log",
maxBytes=1_000_000,
backupCount=3,
encoding="utf-8",
)maxBytes와 backupCount를 모두 지정하면 일정 크기에 도달할 때 파일을 교체해요. 현재 파일과 백업 3개를 유지하고, 새 기록은 계속 report.log에 씁니다. 오래된 기록을 무기한 보존하는 설정은 아니에요. Python 공식 문서
경로는 Path(__file__).resolve().parent를 기준으로 잡았어요. 나중에 작업 스케줄러가 다른 폴더에서 실행해도 .env와 로그 위치가 갑자기 바뀌지 않도록 한 거예요.
📌 2단계: 모든 오류를 재시도하지 않기
인터넷 연결이 잠깐 끊긴 상황과 파일 내용이 잘못된 상황은 다르게 다뤄야 해요. 잘못된 입력을 세 번 처리한다고 저절로 고쳐지지는 않으니까요.
이번 샘플에서는 requests.Timeout과 requests.ConnectionError만 재시도 대상으로 잡았어요. 총 시도 횟수는 3번, 대기는 첫 실패 뒤 1초, 두 번째 실패 뒤 2초입니다. ValueError 같은 나머지 오류는 바로 최종 실패 처리로 넘어갑니다.
실제 업무에 붙일 때는 다시 실행해도 결과가 중복되지 않는 작업부터 적용하세요. 서버가 요청을 처리한 뒤 응답만 끊겼을 수도 있어요. 메일 발송이나 시트 행 추가를 통째로 다시 실행하면 중복 메일·중복 행이 생길 수 있습니다. 읽기 작업을 재시도하거나, 날짜·작업 ID로 기존 결과를 찾아 갱신하는 구조를 먼저 마련하는 게 좋아요.
📌 3단계: 마지막 실패에만 디스코드 알림 보내기
웹훅 URL은 .env에서 읽습니다. 알림에는 작업 이름과 오류 종류만 담고, 전체 예외 메시지나 고객 데이터는 넣지 않아요. 웹훅 URL 자체에도 전송 권한을 가진 토큰이 들어 있으니 코드·로그·캡처에 공개하면 안 됩니다.
wait=true로 Discord의 메시지 생성 응답을 기다리고, allowed_mentions의 parse를 빈 배열로 지정해 불필요한 멘션을 막습니다. Discord 공식 웹훅 문서
연결·읽기 타임아웃은 timeout=(3.05, 10)으로 설정했어요. 이 값은 요청 전체를 정확히 13.05초로 제한한다는 뜻은 아닙니다. 연결과 응답 데이터 대기에 각각 적용돼요. Requests 공식 문서
알림 전송까지 실패하더라도 원래 작업 실패는 그대로 남겨야 해요. 그래서 알림 함수가 요청 오류를 따로 기록하고, 프로그램은 종료 코드 1을 반환하게 했어요. 알림 자체는 자동 재전송하지 않습니다.
📌 전체 코드 — reliable_job.py
아래 코드를 한 파일로 저장하면 돼요. make_demo_job()은 연습용 작업이고, 로그와 재시도, 알림은 그 바깥에서 관리합니다.
"""Python 3.14.7 / requests 2.34.2 / python-dotenv 1.2.3."""
import argparse
import logging
import os
import time
from logging.handlers import RotatingFileHandler
from pathlib import Path
import requests
from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
logger = logging.getLogger("daily_report")
def configure_logging():
log_dir = BASE_DIR / "logs"
log_dir.mkdir(exist_ok=True)
logger.setLevel(logging.INFO)
logger.propagate = False
if logger.handlers:
return
formatter = logging.Formatter(
"%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S"
)
file_handler = RotatingFileHandler(
log_dir / "report.log", maxBytes=1_000_000,
backupCount=3, encoding="utf-8"
)
for handler in (logging.StreamHandler(), file_handler):
handler.setFormatter(formatter)
logger.addHandler(handler)
def run_with_retry(job, attempts=3, delay=1):
for attempt in range(1, attempts + 1):
logger.info("attempt=%s/%s started", attempt, attempts)
try:
return job()
except (requests.Timeout, requests.ConnectionError) as exc:
if attempt == attempts:
raise
wait = delay * (2 ** (attempt - 1))
logger.warning("temporary=%s retry_in=%ss", type(exc).__name__, wait)
time.sleep(wait)
def notify_failure(error_type):
webhook_url = os.getenv("DISCORD_WEBHOOK_URL", "").strip()
if not webhook_url:
logger.warning("notification skipped: DISCORD_WEBHOOK_URL is empty")
return
try:
response = requests.post(
webhook_url,
params={"wait": "true"},
json={
"content": f"업무 리포트 실패 ({error_type}). 서버 로그를 확인하세요.",
"allowed_mentions": {"parse": []},
},
timeout=(3.05, 10),
)
response.raise_for_status()
logger.info("notification sent")
except requests.RequestException as exc:
# URL에 포함된 웹훅 토큰을 로그에 남기지 않습니다.
logger.error("notification failed: %s", type(exc).__name__)
def make_demo_job(mode):
calls = 0
def job():
nonlocal calls
calls += 1
if mode == "fail" or (mode == "retry" and calls < 3):
raise requests.Timeout("simulated timeout")
if mode == "invalid":
raise ValueError("simulated invalid input")
return {"rows": 3}
return job
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--demo", choices=["success", "retry", "fail", "invalid"],
default="success")
parser.add_argument("--notify", action="store_true",
help="최종 실패 때만 실제 Discord 알림을 전송합니다.")
args = parser.parse_args()
configure_logging()
try:
result = run_with_retry(make_demo_job(args.demo))
except Exception as exc:
# 예외 본문에는 URL/고객정보가 들어갈 수 있어 타입만 기록합니다.
logger.error("job failed: %s", type(exc).__name__)
if args.notify:
notify_failure(type(exc).__name__)
else:
logger.info("notification disabled (add --notify to enable)")
return 1
logger.info("job succeeded: rows=%s", result["rows"])
return 0
if __name__ == "__main__":
raise SystemExit(main())핵심 함수는 네 개예요.
- configure_logging(): 화면과 파일에 같은 형식으로 기록해요.
- run_with_retry(): 재시도할 오류와 최대 횟수를 정해요.
- notify_failure(): 최종 실패 알림을 선택적으로 보내요.
- main(): 성공은 0, 실패는 1로 운영체제에 결과를 돌려줘요.
예외 처리가 있다고 실패를 성공처럼 끝내면 안 돼요. raise SystemExit(main())까지 있어야 스케줄러가 종료 코드로 실패를 구분할 수 있습니다.
📌 직접 실행해서 네 가지 결과 확인하기
먼저 정상 동작을 확인해요.
.\.venv\Scripts\python.exe reliable_job.py --demo success
$LASTEXITCODE로그에 attempt=1/3 started, job succeeded: rows=3이 나오고 종료 코드는 0이에요. 여기의 3행은 데모 반환값이며 실제 구글 시트에 쓴 행 수가 아닙니다.
다음은 두 번 실패한 뒤 복구되는 경우예요.
.\.venv\Scripts\python.exe reliable_job.py --demo retry아래는 실제 검증에서 확인한 출력 중 시각을 생략한 내용입니다.
INFO attempt=1/3 started
WARNING temporary=Timeout retry_in=1s
INFO attempt=2/3 started
WARNING temporary=Timeout retry_in=2s
INFO attempt=3/3 started
INFO job succeeded: rows=3실패가 한 번 발생했다고 바로 알림이 나가지는 않아요. 세 번째에 복구됐으므로 성공으로 마칩니다.
.\.venv\Scripts\python.exe reliable_job.py --demo fail
$LASTEXITCODE이 모드는 세 번째 시도까지 실패하고 ERROR job failed: Timeout을 남긴 뒤 1로 종료해요. --notify가 없으므로 notification disabled가 출력되고 실제 전송은 하지 않습니다.
.\.venv\Scripts\python.exe reliable_job.py --demo invalid
$LASTEXITCODE잘못된 입력 모드는 첫 번째 시도에서 곧바로 실패해요. 로그에서 attempt=2/3이 없는지 확인해보세요. 이것이 일시 오류와 입력 오류를 구분한 결과예요.
📌 실제 웹훅을 연결할 때
디스코드에서 알림을 받을 채널의 웹훅 URL을 준비하고 .env의 빈 값에 넣어주세요. .env 파일명을 .env.txt로 저장하지 않도록 주의해요. python-dotenv는 기본적으로 이미 설정된 운영체제 환경변수를 덮어쓰지 않으므로, 예전 환경변수가 남아 있다면 함께 확인하세요. python-dotenv 문서
알림 채널을 확인한 뒤 다음 명령을 실행하면 실제로 실패 알림 한 건을 전송 시도합니다.
.\.venv\Scripts\python.exe reliable_job.py --demo fail --notify메시지 내용은 업무 리포트 실패 (Timeout). 서버 로그를 확인하세요.예요. notification sent는 요청이 정상 응답했다는 뜻이고, 담당자가 읽었거나 휴대전화 푸시를 받았다는 뜻은 아닙니다.
이 글의 검증 범위: 네 가지 데모 실행·종료 코드·1초/2초 재시도 간격·로그 파일 교체를 확인했어요. 웹훅은 모의 응답으로 요청 본문과 오류 처리를 검사했으며, 실제 Discord 채널 도착은 아직 검증하지 않았습니다.
📌 지난 구글 시트 코드에 붙이는 방법
처음에는 지난 편의 collect_today()처럼 데이터를 가져오는 단계만 run_with_retry()로 감싸보세요. 예제에서 잡는 오류는 Requests 계열이므로, 다른 라이브러리를 연결할 때는 그 라이브러리가 던지는 예외에 맞춰 조정해야 해요.
특히 두 부분을 확인해야 합니다. 첫째, 지난 편 main()은 일부 오류를 출력한 뒤 끝내므로 바깥 함수가 실패를 감지하지 못할 수 있어요. 내부에서 잡은 오류를 raise로 다시 전달하거나, 작업 함수를 분리해야 해요. 둘째, gspread의 429·5xx는 이 샘플의 예외 목록만으로 자동 재시도되지 않습니다. 상태 코드와 서버의 재시도 안내에 맞춘 별도 처리가 필요해요.
시트에 쓰는 부분까지 재시도 범위를 넓힐 때는 같은 날짜·작업 ID가 이미 있는지 확인하세요. 이전 편의 날짜별 갱신 방식도 동시에 두 실행이 겹치는 경우까지 중복을 막아주는 것은 아니라는 점을 기억해두면 좋아요.
📌 자주 막히는 부분
ModuleNotFoundError: No module named 'dotenv'
설치한 파이썬과 실행한 파이썬이 다른 경우가 많아요. 준비물 단계와 같은 .\.venv\Scripts\python.exe로 설치와 실행을 모두 해보세요. 패키지 이름은 python-dotenv, 코드의 import 이름은 dotenv예요.
오류는 났는데 디스코드가 조용해요.
--notify 옵션, .env 파일 위치, DISCORD_WEBHOOK_URL을 차례대로 확인하세요. notification failed: HTTPError라면 잘못되거나 삭제된 웹훅, 권한·요청 제한 등을 점검해야 해요. URL 전체나 토큰을 질문 글에 붙이지는 마세요.
원인을 찾기에 로그가 너무 짧아요.
이 예제는 비밀정보 노출을 줄이려고 오류 종류만 기록해요. 실무에서는 입력값이 아니라 작업 단계·실행 ID·HTTP 상태처럼 필요한 진단 항목을 추가하세요. 전체 traceback이 꼭 필요하다면 예외에 토큰이나 개인정보가 포함되는지 확인하고, 접근을 제한한 로컬 로그에 남기는 방식으로 확장하면 됩니다.
📌 마무리
이제 스크립트가 실행된 뒤 어디까지 진행됐는지 기록으로 확인할 수 있어요. 일시 오류는 제한된 횟수만 다시 시도하고, 끝내 실패하면 종료 코드와 선택적 알림으로 드러납니다.
오늘 바로 해볼 것은 하나예요. success, retry, fail, invalid 네 모드를 차례대로 실행해 로그와 종료 코드를 비교해보세요. 성공하는 장면뿐 아니라 실패하는 장면도 알아야 자동화를 맡겨둘 수 있으니까요.
다음에는 이 스크립트를 정해진 시간에 실행하고, 예정된 실행이 빠졌을 때 확인하는 방법으로 이어갈게요. 궁금한 점은 댓글로 남겨주세요! 🙌
연재 · 14편 중 13번째
파이썬 업무 자동화
- #1엑셀 자동화 입문 (openpyxl)!
- #2웹 크롤링 입문 (requests + BeautifulSoup)!
- #3파일 정리 자동화 (이름 일괄 변경)!
- #4디스코드 웹훅(Webhook)으로 알림 보내기 (requests)!
- #5로컬 LLM(LM Studio)으로 고객 문의 자동 분류·요약하기! (무료 AI)
- #6예약 실행 자동화! 매일 정해진 시간에 스크립트 돌리기 (schedule·APScheduler)
- #7동적 페이지·로그인 자동화! requests로 안 되는 사이트 긁기 (Selenium)
- #8PDF 자동 처리! 여러 파일 병합·분할부터 비밀번호까지 (pypdf) 📄
- #9마우스·키보드 GUI 자동화! 클릭·타이핑을 파이썬이 대신하게 하기 (PyAutoGUI)
- #10워드(Word) 문서 자동 생성! 엑셀 명단으로 계약서·안내문 100장 뽑기 (python-docx) 📄
- #11개인화 대량 메일 자동 발송! 명단별 첨부파일·발송 로그·재시도까지 (smtplib 실전편) 📬
- #12구글 시트 연동하기! 팀원과 같이 보는 자동 시트 (gspread) 📗
- #13파이썬 자동화 로그·재시도·디스코드 실패 알림 만들기 - 조용히 멈추는 스크립트 잡기지금 읽는 글
- #14파워포인트(PowerPoint) 보고서 자동 생성! 엑셀 숫자만 바꾸면 발표자료 완성 (python-pptx) 📊
