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에는 메일 서버가 없어서 로그를 파일로 남기는 게 실용적입니다.

장점

  • 별도 인프라 비용 0원: 이미 돌아가는 내 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가 스케줄에 맞춰 컨테이너를 띄우고, 끝나면 자동 종료합니다. 내 로컬 상태와 무관합니다.
  • 환경변수 주입이 선언적: 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로 파이프라인을 구성한 과정을 정리하겠습니다.