Published on

주간 스케줄링 — 로컬 crontab vs Render Cron Job 비교

Authors
  • avatar
    Name
    Hyo814
    Twitter

주간 스케줄링 — 로컬 crontab vs Render Cron Job 비교

PyNews 프로젝트에서는 매주 월요일 오전 9시에 RSS 피드를 크롤링해 DB에 적재해야 했습니다. 개발 초기에는 로컬 Mac에서 crontab으로 돌리다가 프로덕션 배포 시점에 Render Cron Job으로 옮겼고, 같은 목적을 두 방식으로 구현하면서 느낀 차이를 적어둡니다.


1. 공통 부분 — 실행하는 명령은 동일

두 방식 모두 결국 같은 Django management command를 호출합니다.

python manage.py crawl_news --days=7

스케줄러는 "언제, 어디서, 어떤 환경에서" 이 명령을 실행할지를 결정할 뿐, 실제 크롤링 로직은 똑같습니다. 이 구조 덕분에 로컬에서 Render로 옮길 때 큰 마찰이 없었습니다.


2. 로컬 crontab — setup_cron.sh

프로젝트 루트의 setup_cron.sh를 한 번 실행하면 Mac/Linux의 사용자 crontab에 작업이 등록됩니다.

#!/bin/bash
# 매주 월요일 오전 9시에 크롤링 실행
# 사용법: bash setup_cron.sh

PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHON="$PROJECT_DIR/venv/bin/python"
MANAGE="$PROJECT_DIR/manage.py"
LOG="$PROJECT_DIR/crawl.log"

CRON_CMD="0 9 * * 1 cd $PROJECT_DIR && $PYTHON $MANAGE crawl_news --days=7 >> $LOG 2>&1"

# 기존 크론에 이미 등록되어 있는지 확인
if crontab -l 2>/dev/null | grep -q "crawl_news"; then
    echo "이미 등록된 크론 작업이 있습니다."
    crontab -l | grep "crawl_news"
else
    (crontab -l 2>/dev/null; echo "$CRON_CMD") | crontab -
    echo "크론 작업이 등록되었습니다:"
    echo "$CRON_CMD"
fi

설계 포인트는 세 가지입니다.

  • 절대경로 사용. PYTHON="$PROJECT_DIR/venv/bin/python"처럼 프로젝트 내 venv를 직접 가리킵니다. cron은 로그인 셸이 아니라서 PATH가 최소한이라, which python을 기대하면 실패합니다.
  • 중복 등록 방지. crontab -l | grep -q "crawl_news"로 이미 등록돼 있는지 먼저 확인합니다. 스크립트를 여러 번 돌려도 크론 항목이 쌓이지 않습니다.
  • 로그 리다이렉션. >> $LOG 2>&1로 stdout/stderr를 파일로 모읍니다. cron은 기본적으로 실행 결과를 메일로 보내는데, 로컬 Mac에는 메일 서버가 없어서 로그를 파일로 남기는 쪽이 실용적입니다.

로컬 crontab의 강점

별도 인프라 비용이 들지 않습니다. 이미 돌아가는 Mac/서버의 crontab만 쓰면 됩니다. 디버깅도 빠릅니다. 로그 파일이 바로 옆에 있어서 tail -f crawl.log로 즉시 확인됩니다.

한계

  • 기계가 꺼지면 안 돌아갑니다. Mac을 끄거나 잠자기 상태면 당연히 실행되지 않아서, 노트북을 스케줄러로 쓰는 건 프로덕션에 부적합합니다.
  • 환경 의존성이 강합니다. venv 경로, 환경변수, DB 접속 정보가 로컬 환경에 묶여서 다른 머신으로 옮기면 전부 다시 세팅해야 합니다.
  • Django secret, DB URL 같은 민감 정보가 로컬 .env에 남게 됩니다.

3. Render Cron Job — render.yaml

프로덕션에서는 Render의 type: cron 서비스로 대체했습니다.

- type: cron
  name: pynews-crawler
  runtime: python
  plan: starter
  schedule: "0 0 * * 1"
  buildCommand: pip install -r requirements.txt
  startCommand: python manage.py crawl_news --days=7
  envVars:
    - key: DATABASE_URL
      fromDatabase:
        name: pynews-db
        property: connectionString

구조는 crontab보다 오히려 단순합니다. "언제(schedule)", "어떤 명령을(startCommand)" 두 줄이면 끝납니다.

Render의 강점

언제든 실행됩니다. Render가 스케줄에 맞춰 컨테이너를 띄우고, 끝나면 자동 종료합니다. 내 로컬 상태와 무관합니다. 환경변수 주입도 선언적입니다. fromDatabase로 DB 연결 문자열을 받고, SECRET_KEYgenerateValue: true로 자동 생성하므로 민감 정보를 코드에 하드코딩하지 않아도 됩니다. 수동 트리거도 됩니다. 대시보드의 "Trigger Run" 버튼으로 스케줄을 무시하고 즉시 실행할 수 있어서 배포 직후 동작 확인할 때 편합니다.

한계

  • 비용이 듭니다. free 플랜에서는 cron 서비스를 지원하지 않아 starter 이상 플랜이 필요합니다.
  • 시간 기준이 UTC입니다. schedule: "0 0 * * 1"은 UTC 월요일 00:00, KST로는 월요일 오전 9시입니다. 한국 시간 감각으로 사고하면 틀리기 쉽습니다.
  • 로그 접근성이 떨어집니다. Render 대시보드를 열어야 로그를 보는데, 로컬 tail -f만큼 즉각적이지는 않습니다.

4. 같은 크론 표현식이지만 의미가 다른 부분

주의할 점이 하나 있습니다. 두 스케줄 문자열이 살짝 다릅니다.

# 로컬 crontab — 서버 로컬타임 기준 (KST로 세팅된 Mac이라면 월요일 9시)
0 9 * * 1

# Render cron — UTC 기준 (KST 월요일 9시를 만들려면 UTC 00:00)
0 0 * * 1

같은 "월요일 오전 9시 KST"인데 표현이 다릅니다. 로컬은 시스템 타임존을 따르고, Render는 UTC 고정입니다. 타임존에 따라 크론 표현식이 달라진다는 것이 이관할 때 가장 헷갈렸습니다.


5. 두 방식을 모두 유지한 이유

setup_cron.sh를 Render로 옮긴 뒤에도 지우지 않고 남겨뒀습니다.

  • 로컬에서 크롤링 로직을 개발/테스트할 때 여전히 유용합니다.
  • Render cron은 실행 주기가 길어서(주 1회) 개발 중에 반복 실행하기에는 어색합니다.
  • 같은 명령을 로컬과 프로덕션 양쪽에서 돌려볼 수 있다는 것 자체가 management command 방식의 이점입니다.

6. 선택 기준

상황추천
개인 프로젝트, 항상 켜진 서버 없음Render Cron Job (또는 GitHub Actions 스케줄)
이미 24시간 운영 중인 VPS/서버가 있음crontab
개발 중 반복 실행하며 디버깅로컬 crontab 또는 수동 실행
프로덕션 데이터 적재Render Cron Job (안정성/로그 관리)

정리

crontab과 Render Cron Job은 결국 같은 명령을 돌리지만, 내 머신의 상태에 의존하느냐가 가장 큰 차이였습니다. 로컬 crontab은 빠른 개발과 디버깅에, Render Cron Job은 프로덕션 안정성에 각각 강점이 있고, Django management command로 로직을 분리해두면 두 방식 모두 같은 코드로 돌릴 수 있습니다.

다음 글에서는 이 crawl_news 명령이 실제로 11개 RSS 소스에서 데이터를 어떻게 수집하는지, feedparser와 BeautifulSoup4로 파이프라인을 짠 과정을 적어보겠습니다.