파이썬 자동화 #6 스케줄링: 내가 자는 동안 돌게 하기
5편까지 따라왔다면 데이터를 모으고, 엑셀로 정리하고, 결과를 메일이나 메신저로 보내주는 스크립트가 손에 있습니다. 그런데 아직 한 가지가 남아 있습니다. 그 스크립트의 실행 버튼을 매일 누르는 사람이 여전히 나라는 점입니다. 실행까지 자동화해야 진짜 자동화입니다. 이번 글에서는 스크립트를 정해진 시간에 알아서 돌게 만드는 방법을 정리하겠습니다.
- #1 스크립트 첫걸음
- #2 엑셀 자동화
- #3 웹 스크래핑: 정적 페이지
- #4 웹 스크래핑: 동적 페이지
- #5 메일·알림
- #6 스케줄링 ← 이번 글
- #7 CLI 도구로 포장
방법은 크게 둘로 나뉩니다. cron이나 작업 스케줄러처럼 OS가 시간을 재고 스크립트를 깨워주는 방식과, APScheduler처럼 파이썬 프로세스가 항상 떠 있으면서 직접 시간을 재는 방식입니다. 먼저 대부분의 단발성 자동화에 충분한 OS 스케줄러부터 보겠습니다.
cron: macOS와 리눅스의 기본 #
cron은 유닉스 계열 OS에 수십 년째 들어 있는 스케줄러입니다. 터미널에서 crontab -e로 편집기를 열고, 한 줄에 하나씩 “시간 필드 다섯 개 + 실행할 명령"을 등록합니다.
| 필드 | 의미 | 허용 값 |
|---|---|---|
| 1 | 분 | 0〜59 |
| 2 | 시 | 0〜23 |
| 3 | 일 | 1〜31 |
| 4 | 월 | 1〜12 |
| 5 | 요일 | 0〜7 (0과 7이 일요일) |
*는 “매번”, */5는 “5마다”, 1-5는 범위입니다. 평일 아침 7시에 일일 리포트를 돌리는 줄은 이렇습니다.
0 7 * * 1-5 /home/me/scripts/.venv/bin/python /home/me/scripts/daily_report.py >> /home/me/scripts/cron.log 2>&1뒤에 붙인 >>는 표준 출력을 파일에 이어 붙이고, 2>&1은 에러 출력까지 같은 파일로 보내는 처리입니다. 처음 등록할 때 이렇게 출력을 받아두면 디버깅이 훨씬 쉬워집니다. 그리고 여기서 가장 많이 부딪히는 함정이 환경변수입니다. cron은 내 터미널의 .zshrc나 .bashrc를 읽지 않고, PATH가 거의 비어 있는 상태로 명령을 실행합니다. 터미널에서는 잘 되던 python daily_report.py가 cron에서는 “command not found"로 죽는 이유입니다. 대응 원칙은 하나, 전부 절대 경로로 적는 것입니다.
- 파이썬 실행 파일과 스크립트:
python daily_report.py가 아니라 가상환경 안의/home/me/scripts/.venv/bin/python /home/me/scripts/daily_report.py - 스크립트가 읽고 쓰는 파일: cron의 작업 디렉터리는 내 프로젝트 폴더가 아니므로, 상대 경로 대신
pathlib.Path(__file__).parent기준으로 만듭니다
macOS의 launchd, 윈도우의 작업 스케줄러 #
macOS에서도 crontab은 동작하지만, 애플이 공식으로 미는 방식은 launchd입니다. 실용적인 차이는 잠자기 처리입니다. cron은 지정 시각에 맥이 잠들어 있으면 그 회차를 그냥 건너뛰지만, launchd의 StartCalendarInterval은 깨어났을 때 놓친 작업을 한 번 실행해 줍니다. 설정이 plist 파일 작성이라 cron보다 장황하므로, 노트북에서 돌릴 때 이 차이가 필요해지면 찾아보는 정도로 기억해 두면 됩니다.
윈도우에는 cron 대신 작업 스케줄러(Task Scheduler)가 있습니다. GUI에서 “기본 작업 만들기"를 누르고 트리거(매일 오전 7시), 동작(프로그램 칸에 C:\Users\me\scripts\.venv\Scripts\python.exe, 인수 칸에 스크립트 경로), 시작 위치(스크립트 폴더)를 지정하면 끝입니다. 절대 경로 원칙은 윈도우에서도 똑같이 적용됩니다.
파이썬 안에서 해결하기: APScheduler #
OS 스케줄러가 “OS가 스크립트를 깨우는” 구조라면, pip install apscheduler로 설치하는 APScheduler는 파이썬 프로세스가 직접 시간을 재는 구조입니다.
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job("cron", day_of_week="mon-fri", hour=7, minute=0)
def daily_report():
... # 5편까지 만든 작업 함수를 여기서 호출합니다
scheduler.start()실행하면 프로세스가 종료되지 않고 떠 있으면서 등록한 시각마다 함수를 호출합니다. cron 문법을 그대로 쓰는 cron 트리거 외에 “30분마다” 식의 interval 트리거도 있고, 같은 파이썬 코드라 가상환경이나 경로 문제가 없습니다. 대신 결정적인 차이가 있습니다. 이 프로세스가 죽으면 모든 스케줄이 멈춘다는 점입니다. 터미널을 닫거나 재부팅하면 끝입니다. cron은 OS가 살아 있는 한 동작하지만, APScheduler는 프로세스 생존을 내가 책임져야 합니다.
어느 쪽을 쓸까 #
| 상황 | 추천 |
|---|---|
| 하루 한두 번 도는 단발성 스크립트 | cron / 작업 스케줄러 |
| 노트북이라 잠자기가 잦음 | launchd (macOS) |
| 항상 떠 있는 앱(봇, 웹 서버) 안의 주기 작업 | APScheduler |
| 내 컴퓨터를 켜 둘 수 없음 | GitHub Actions (아래) |
돌았는지 확인하기: print 대신 logging #
스케줄러에 올리는 순간 스크립트는 내 눈앞에서 돌지 않습니다. print는 터미널이 없으면 갈 곳이 없으므로, 실행 기록을 파일로 남기는 logging으로 바꿉니다.
import logging
from pathlib import Path
logging.basicConfig(
filename=Path(__file__).parent / "automation.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s")로그 파일 경로도 Path(__file__).parent 기준 절대 경로로 만든 점에 주의하세요. 작업 곳곳에서 logging.info("32건 처리")처럼 남겨두면, 아침에 automation.log를 열어 어젯밤에 돌았는지, 몇 건을 처리했는지 바로 확인할 수 있습니다.
실패 대비: 죽으면 알림으로 받기 #
자동화가 무서운 순간은 실패할 때가 아니라 실패한 줄 모르고 지나갈 때입니다. 사이트 구조가 바뀌어 스크래핑이 죽었는데 한 달 뒤에 알게 되는 상황이 전형적입니다. 5편에서 만든 알림 함수를 재활용해, 메인 로직을 감싸는 래퍼를 둡니다.
import logging
import traceback
from notify import send_message # 5편에서 만든 알림 함수
if __name__ == "__main__":
try:
main()
logging.info("작업 성공")
except Exception:
logging.exception("작업 실패")
send_message(f"[자동화 실패] daily_report\n{traceback.format_exc()[-500:]}")
raise성공하면 로그 한 줄, 실패하면 로그에 전체 traceback을 남기고 메신저로 마지막 500자를 보냅니다. 끝의 raise는 예외를 다시 던져 종료 코드를 0이 아니게 만들어, 스케줄러 쪽에도 실패로 기록되게 합니다.
서버 없이 공짜로: GitHub Actions #
지금까지의 방법은 전부 내 컴퓨터가 켜져 있어야 합니다. 컴퓨터를 끄고 자고 싶다면 GitHub Actions의 schedule 트리거가 가장 간단한 무료 선택지입니다. 스크립트를 저장소에 올리고, .github/workflows/daily.yml 파일 하나를 추가합니다.
name: daily-report
on:
schedule:
- cron: "0 22 * * *" # UTC 기준이라 KST 오전 7시
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: pip install -r requirements.txt
- run: python daily_report.pycron 문법을 그대로 쓰되 세 가지를 기억해야 합니다. 시각이 UTC 기준이라 KST에서 9시간을 빼야 하고, 정확한 시각 실행이 보장되지 않아 몇 분에서 수십 분 늦게 돌 수 있고, 저장소에 60일간 활동이 없으면 스케줄이 자동으로 비활성화됩니다. 토큰이나 웹훅 주소 같은 비밀 값은 코드에 적지 말고 저장소 Settings의 Secrets에 넣어 환경변수로 받습니다.
정리 #
이번 글에서 만든 흐름을 정리하겠습니다.
- 단발성 스크립트는 OS 스케줄러(cron / 작업 스케줄러)로, 함정인 환경변수와 경로는 절대 경로 통일로 해결
- 항상 떠 있는 앱 안의 주기 작업은 APScheduler
print대신logging으로 실행 기록을 파일에 남기기- try/except 래퍼로 실패를 5편의 알림에 연결해 “모르고 지나가는 실패” 차단
- 내 컴퓨터를 끄고 싶다면 GitHub Actions
schedule
다음 글(#7 CLI 도구로 포장)이 이 시리즈의 마지막입니다. 지금까지 만든 스크립트들에 argparse로 옵션을 달고, 하나의 명령줄 도구로 묶어 어디서든 한 단어로 부를 수 있게 포장하겠습니다.