터미널에서 직접 실행하면 멀쩡히 돌아가는 스크립트가, 크론탭에 등록만 하면 아무 일도 일어나지 않습니다. 에러 메시지도 없고 로그도 안 남습니다. 크론잡 실행 안됨 문제가 특히 골치 아픈 이유는 실패가 조용하기 때문입니다. 백업이 사흘째 안 돌고 있었다는 걸 정작 복구가 필요한 날에야 알게 되는 식이죠. 이 글에서는 리눅스 크론과 윈도우 작업 스케줄러 양쪽에서, 원인을 좁혀 나가는 순서를 정리했습니다. 위에서부터 차례로 확인하면 대부분 10분 안에 잡힙니다.
0단계: 스크립트가 아예 실행되긴 했는가
제일 먼저 갈라야 할 갈림길입니다. 실행 자체가 안 된 것과 실행은 됐는데 중간에 죽은 것은 원인이 완전히 다릅니다. 이걸 구분하지 않고 스크립트 코드부터 뜯어보면 시간만 버립니다.
# 우분투 / 데비안 계열
grep CRON /var/log/syslog | tail -20
# CentOS / RHEL / Rocky 계열
sudo tail -20 /var/log/cron
# systemd를 쓰는 대부분의 최신 배포판 (위 두 개가 비어 있을 때)
journalctl -u cron --since "1 hour ago"
journalctl -u crond --since "1 hour ago" # RHEL 계열은 서비스명이 crond
# 크론 데몬 자체가 살아 있는지
systemctl status cron # 또는 crond
로그에 CRON[12345]: (james) CMD (...) 같은 줄이 보인다면 크론은 제 할 일을 한 겁니다. 그러면 원인은 스크립트 쪽, 즉 아래 1~3단계입니다. 반대로 아무 줄도 없다면 등록 자체가 잘못됐거나 데몬이 죽어 있는 겁니다.
윈도우라면 작업 스케줄러를 열고 해당 작업의 기록 탭을 봅니다. 탭이 비어 있으면 오른쪽 모든 작업 기록 사용을 먼저 켜야 합니다. 기본적으로 꺼져 있는 환경이 많아서, 이것 때문에 “기록이 없으니 실행이 안 된 것”으로 오해하기 쉽습니다.
1단계: 환경변수가 다르다 (가장 흔한 원인)
크론은 여러분의 로그인 셸이 아닙니다. .bashrc도, .bash_profile도 읽지 않습니다. 그래서 터미널에서 잘 잡히던 python, node, aws 같은 명령을 크론은 못 찾습니다. 크론의 기본 PATH는 대개 /usr/bin:/bin뿐입니다.
짐작하지 말고 직접 확인하는 방법이 있습니다.
# 크론이 실제로 어떤 환경에서 도는지 직접 찍어본다.
# crontab -e 로 아래 한 줄을 넣고 1분 기다린 뒤 /tmp/cronenv.txt 를 열어본다.
* * * * * env > /tmp/cronenv.txt 2>&1
# 그리고 내 터미널의 환경과 비교한다.
env > /tmp/myenv.txt
diff /tmp/cronenv.txt /tmp/myenv.txt
출력된 차이를 보면 대부분 답이 나옵니다. PATH가 짧고, VIRTUAL_ENV가 없고, API 키를 담아둔 커스텀 변수가 통째로 비어 있을 겁니다. 해결책은 두 가지입니다.
- 명령을 전부 절대 경로로 쓴다. 가장 확실한 방법입니다.
- crontab 맨 위에
PATH를 직접 선언한다. 줄이 여러 개일 때 편합니다.
# 나쁜 예 — 터미널에서만 되는 줄
0 9 * * * python backup.py
# 좋은 예 — 인터프리터도, 스크립트도, 작업 디렉터리도 전부 명시
0 9 * * * cd /home/james/tools && /home/james/venv/bin/python /home/james/tools/backup.py >> /home/james/logs/backup.log 2>&1
# 내 python이 어디 있는지 모르겠다면
which python3
readlink -f $(which python3)
가상환경을 쓴다면 activate를 호출할 필요가 없습니다. 그 환경 안의 파이썬 실행 파일(venv/bin/python)을 직접 가리키면 activate와 동일한 효과가 납니다.
2단계: 작업 디렉터리가 다르다
크론은 스크립트를 사용자 홈 디렉터리에서 실행합니다. 윈도우 작업 스케줄러는 시작 위치를 비워두면 C:\Windows\System32에서 실행합니다. 둘 다 여러분이 스크립트를 두고 개발한 그 폴더가 아닙니다.
그래서 open("config.json") 같은 상대 경로는 전부 어긋납니다. 파일을 못 찾고, 로그도 엉뚱한 곳에 쌓이고, 결과 파일은 어디로 갔는지 알 수 없게 됩니다.
# 나쁜 예 — 실행 위치에 따라 파일을 못 찾는다
with open("config.json") as f:
cfg = json.load(f)
# 좋은 예 — 스크립트 파일 자신의 위치를 기준으로 삼는다
from pathlib import Path
import json
BASE = Path(__file__).resolve().parent
with open(BASE / "config.json", encoding="utf-8") as f:
cfg = json.load(f)
LOG = BASE / "logs" / "run.log"
LOG.parent.mkdir(parents=True, exist_ok=True)
Path(__file__).resolve().parent는 어디서 실행하든 항상 스크립트 파일이 놓인 폴더를 가리킵니다. 자동화용 스크립트라면 처음부터 이 패턴으로 써두는 편이 좋습니다. 크론 줄에서 cd로 옮겨주는 방법도 함께 쓰면 더 안전합니다.
3단계: 크론 표기법의 함정
문법은 맞는데 안 도는 경우들이 있습니다. 아래 세 가지가 특히 자주 걸립니다.
# crontab 맨 위에 PATH를 직접 선언해두면 줄마다 절대경로를 쓰지 않아도 된다.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/james/.local/bin
MAILTO=""
# %는 크론에서 줄바꿈으로 해석되므로 반드시 백슬래시로 이스케이프한다.
# date +%Y-%m-%d 를 그대로 쓰면 %Y 앞에서 명령이 잘린다.
0 3 * * * /usr/bin/tar czf /backup/db-$(date +\%Y\%m\%d).tar.gz /var/lib/data
| 증상 | 원인 | 해결 |
|---|---|---|
date +%Y가 있는 줄만 실행 안 됨 | 크론에서 %는 줄바꿈 기호라 그 앞에서 명령이 잘림 | \%로 이스케이프 |
| 맨 마지막 줄만 실행 안 됨 | 파일 끝에 개행 문자가 없음 | 마지막 줄 뒤에 빈 줄 하나 추가 |
@reboot이 안 먹음 | 부팅 시점에 네트워크·마운트가 아직 준비 전 | sleep 60을 앞에 붙이거나 systemd 유닛으로 전환 |
| 다른 사용자로 등록했는데 안 보임 | crontab -e는 현재 사용자 것만 편집 | sudo crontab -l -u 사용자명으로 확인 |
시간 표기가 헷갈린다면 0 9 * * *(매일 9시), */10 * * * *(10분마다), 0 3 * * 1(매주 월요일 3시) 세 패턴만 외워두면 실무의 대부분이 커버됩니다. 요일은 0과 7이 모두 일요일입니다.
4단계: 윈도우 작업 스케줄러 전용 점검
윈도우는 원인이 조금 다릅니다. 명령줄에서 상태부터 확인하는 게 GUI를 헤매는 것보다 빠릅니다.
:: 등록된 작업의 마지막 실행 시각과 결과를 한눈에 본다
schtasks /Query /TN "내작업이름" /V /FO LIST
:: 결과에서 볼 항목
:: 마지막 실행 시간 (Last Run Time) -> 아예 안 돌았는지 확인
:: 마지막 결과 (Last Result) -> 0이 아니면 실패
:: 실행할 작업 (Task To Run) -> 경로에 공백이 있으면 따옴표 확인
:: 시작 위치 (Start In) -> 비어 있으면 상대경로가 깨진다
:: 지금 즉시 한 번 돌려보기 (예약 시각까지 기다리지 않아도 된다)
schtasks /Run /TN "내작업이름"

마지막 결과 코드는 다음처럼 읽습니다.
| 코드 | 의미 | 흔한 원인 |
|---|---|---|
| 0x0 | 정상 종료 | 성공. 그런데 결과물이 없다면 스크립트 내부 로직 문제 |
| 0x1 | 일반 오류 | 스크립트가 0이 아닌 코드로 종료. 로그를 봐야 함 |
| 0x2 | 파일을 찾을 수 없음 | 실행 파일 경로 오타, 또는 경로에 공백이 있는데 따옴표 누락 |
| 0x8007010B | 디렉터리 이름이 잘못됨 | 시작 위치(Start In)가 비었거나 잘못됨 |
| 0x41301 | 현재 실행 중 | 이전 회차가 안 끝남. 중복 실행 정책 확인 |
| 0x41303 | 아직 실행된 적 없음 | 트리거가 등록되지 않았거나 작업이 사용 안 함 상태 |
| 0x41306 | 작업이 종료됨 | 실행 시간 제한 초과로 강제 종료 |
여기에 더해 윈도우에서만 걸리는 항목이 셋 있습니다.
- “사용자의 로그온 여부에 관계없이 실행”을 골랐다면 계정 비밀번호를 저장해야 합니다. 비밀번호를 바꾼 뒤 작업이 조용히 멈추는 사고의 90%가 이것입니다.
- “가장 높은 수준의 권한으로 실행”이 필요한 작업인지 확인합니다. 관리자 폴더에 쓰는 스크립트라면 필수입니다.
- “컴퓨터의 AC 전원이 켜져 있는 경우에만 작업 시작”이 기본 체크되어 있습니다. 노트북에서 배터리로 돌 때 작업이 건너뛰어지는 원인입니다.
실패를 조용히 넘기지 않게 만들기
원인을 찾았더라도, 다음번에 또 조용히 실패하면 같은 자리로 돌아옵니다. 크론에는 스크립트를 직접 걸지 말고 로그를 남기는 래퍼를 거는 습관을 들이는 게 좋습니다.
#!/bin/bash
# run-backup.sh — 크론에는 이 래퍼만 등록한다.
# 목적: 실패했을 때 "조용히 아무 일도 안 일어난" 상태를 없애는 것.
set -euo pipefail
BASE="/home/james/tools"
LOG="$BASE/logs/backup-$(date +%Y%m%d).log"
mkdir -p "$BASE/logs"
{
echo "===== 시작: $(date "+%F %T") ====="
cd "$BASE"
/home/james/venv/bin/python backup.py
echo "===== 정상 종료: $(date "+%F %T") ====="
} >> "$LOG" 2>&1
STATUS=$?
if [ "$STATUS" -ne 0 ]; then
echo "backup 실패 (exit $STATUS). 로그: $LOG" >> "$BASE/logs/ERRORS.log"
fi
윈도우라면 배치 파일이 같은 역할을 합니다. %~dp0가 배치 파일 자신의 폴더를 가리키므로, 시작 위치 설정을 깜빡해도 안전합니다.
:: run-backup.bat — 작업 스케줄러에는 이 배치 파일을 등록한다.
@echo off
chcp 65001 > nul
:: %~dp0 는 이 배치 파일이 있는 폴더. 시작 위치가 뭐든 항상 올바른 곳으로 이동한다.
cd /d "%~dp0"
if not exist "logs" mkdir "logs"
set LOGFILE=logs\backup-%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%.log
echo ===== 시작 %DATE% %TIME% ===== >> "%LOGFILE%"
python.exe "%~dp0backup.py" >> "%LOGFILE%" 2>&1
echo 종료코드=%ERRORLEVEL% >> "%LOGFILE%"
>> log 2>&1에서2>&1을 빼먹으면 정상 출력만 저장되고 에러 메시지는 그대로 버려집니다. 정작 필요한 건 에러 쪽인데 말이죠. 순서도 중요합니다.2>&1 >> log처럼 뒤집어 쓰면 의도대로 동작하지 않습니다.
로그를 남긴 다음에는 실패를 알려줄 통로가 필요합니다. 텔레그램이나 슬랙 웹훅으로 한 줄 보내는 것만으로 충분합니다. 이전 글에서 다룬 카카오톡/텔레그램 알림 자동 발송 방식을 래퍼 스크립트의 실패 분기에 붙이면 됩니다.
10분 체크리스트
문제가 생겼을 때 위에서부터 순서대로 확인하세요.
- 크론 로그(
grep CRON /var/log/syslog) 또는 스케줄러 기록 탭에 실행 흔적이 있는가 - 크론 데몬 / 작업 스케줄러 서비스가 살아 있는가
- 스크립트에 실행 권한이 있는가 (
chmod +x), 셔뱅(#!/usr/bin/env python3)이 맞는가 - 명령이 절대 경로인가, 아니면 crontab 상단에
PATH를 선언했는가 - 스크립트 안의 파일 경로가 전부 절대 경로이거나
__file__기준인가 - 크론 줄에
%가 이스케이프되지 않은 채 들어 있지 않은가 - 파일 마지막 줄 뒤에 개행이 있는가
- 출력이
>> log 2>&1로 저장되고 있는가 - (윈도우) 시작 위치가 채워져 있고, 계정 비밀번호가 저장되어 있는가
- 수동 실행(
schtasks /Run또는 크론 줄 그대로 복사해 실행)으로는 성공하는가
마지막 항목이 핵심입니다. 크론 줄에 적은 명령을 토씨 하나 안 바꾸고 터미널에 붙여넣어 실행해보세요. 이때 실패한다면 크론 문제가 아니라 명령 자체의 문제이고, 성공한다면 환경 차이(1~2단계)가 범인입니다.
마무리
오늘 당장 할 수 있는 가장 작은 조치 하나만 고르라면, 지금 돌고 있는 크론잡 한 줄 끝에 >> ~/logs/작업이름.log 2>&1을 붙이는 것입니다. 그것만으로도 다음번 실패는 조용하지 않게 됩니다.
다음 글에서는 여러 개의 PDF 파일을 자동으로 합치고 분할하는 스크립트를 만들어 보겠습니다.