파이썬 자동화 #6 스케줄링: 내가 자는 동안 돌게 하기

6 분 소요

5편까지 따라왔다면 데이터를 모으고, 엑셀로 정리하고, 결과를 메일이나 메신저로 보내주는 스크립트가 손에 있습니다. 그런데 아직 한 가지가 남아 있습니다. 그 스크립트의 실행 버튼을 매일 누르는 사람이 여전히 나라는 점입니다. 실행까지 자동화해야 진짜 자동화입니다. 이번 글에서는 스크립트를 정해진 시간에 알아서 돌게 만드는 방법을 정리하겠습니다.

  • #1 스크립트 첫걸음
  • #2 엑셀 자동화
  • #3 웹 스크래핑: 정적 페이지
  • #4 웹 스크래핑: 동적 페이지
  • #5 메일·알림
  • #6 스케줄링 ← 이번 글
  • #7 CLI 도구로 포장

방법은 크게 둘로 나뉩니다. cron이나 작업 스케줄러처럼 OS가 시간을 재고 스크립트를 깨워주는 방식과, APScheduler처럼 파이썬 프로세스가 항상 떠 있으면서 직접 시간을 재는 방식입니다. 먼저 대부분의 단발성 자동화에 충분한 OS 스케줄러부터 보겠습니다.

cron: macOS와 리눅스의 기본 #

cron은 유닉스 계열 OS에 수십 년째 들어 있는 스케줄러입니다. 터미널에서 crontab -e로 편집기를 열고, 한 줄에 하나씩 “시간 필드 다섯 개 + 실행할 명령"을 등록합니다.

필드의미허용 값
10〜59
20〜23
31〜31
41〜12
5요일0〜7 (0과 7이 일요일)

*는 “매번”, */5는 “5마다”, 1-5는 범위입니다. 평일 아침 7시에 일일 리포트를 돌리는 줄은 이렇습니다.

crontab 예시
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는 파이썬 프로세스가 직접 시간을 재는 구조입니다.

scheduler.py
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으로 바꿉니다.

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 파일 하나를 추가합니다.

.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.py

cron 문법을 그대로 쓰되 세 가지를 기억해야 합니다. 시각이 UTC 기준이라 KST에서 9시간을 빼야 하고, 정확한 시각 실행이 보장되지 않아 몇 분에서 수십 분 늦게 돌 수 있고, 저장소에 60일간 활동이 없으면 스케줄이 자동으로 비활성화됩니다. 토큰이나 웹훅 주소 같은 비밀 값은 코드에 적지 말고 저장소 Settings의 Secrets에 넣어 환경변수로 받습니다.

정리 #

이번 글에서 만든 흐름을 정리하겠습니다.

  • 단발성 스크립트는 OS 스케줄러(cron / 작업 스케줄러)로, 함정인 환경변수와 경로는 절대 경로 통일로 해결
  • 항상 떠 있는 앱 안의 주기 작업은 APScheduler
  • print 대신 logging으로 실행 기록을 파일에 남기기
  • try/except 래퍼로 실패를 5편의 알림에 연결해 “모르고 지나가는 실패” 차단
  • 내 컴퓨터를 끄고 싶다면 GitHub Actions schedule

다음 글(#7 CLI 도구로 포장)이 이 시리즈의 마지막입니다. 지금까지 만든 스크립트들에 argparse로 옵션을 달고, 하나의 명령줄 도구로 묶어 어디서든 한 단어로 부를 수 있게 포장하겠습니다.

X