* 는 "아무 값이나"
30 * * * *는 "분이 30일 때"만 조건이고 나머지는 무조건 통과라 매시 30분에 돕니다. 매시간이라는 뜻이 아니라 분 조건만 걸었다는 뜻입니다.
브라우저 내부 처리
표현식 문법은 5분이면 익힙니다. 문제는 문법이 맞는데도 안 도는 경우입니다. 스케줄러를 운영하면서 실제로 장애가 났던 지점을 순서대로 정리했습니다.
왼쪽부터 분, 시, 일, 월, 요일입니다. 이 순서를 외우는 것보다 각 칸이 "언제"가 아니라 "어떤 값일 때"를 뜻한다는 걸 이해하는 게 빠릅니다. cron은 매분 깨어나서 지금 시각이 다섯 칸의 조건에 모두 맞는지 확인하고, 맞으면 실행합니다.
30 * * * *는 "분이 30일 때"만 조건이고 나머지는 무조건 통과라 매시 30분에 돕니다. 매시간이라는 뜻이 아니라 분 조건만 걸었다는 뜻입니다.
*/15는 "15로 나눈 나머지가 0"이라 0, 15, 30, 45분입니다. 15분 간격이 아니라 정해진 시각입니다. 7 */3 * * *는 3시간마다가 아니라 0·3·6·9·12·15·18·21시 7분입니다.
0 9-18/2 * * 1-5는 평일 9시부터 18시까지 두 시간 간격 정시입니다. 0 0,12 * * *처럼 콤마로 개별 값을 나열해도 됩니다.
여기까지가 문법이고, 아래부터가 실제로 사고가 나는 지점입니다. Cron 표현식 파서에 표현식을 넣어보면 다음 실행 시각이 나오니 읽으면서 같이 확인하시면 빠릅니다.
이게 cron에서 가장 많이 틀리는 지점이고, 문법 오류가 아니라 조용히 다르게 도니까 배포 후에야 발견됩니다.
세 번째 칸(일)과 다섯 번째 칸(요일)을 둘 다 * 이 아닌 값으로 지정하면, 대부분의 cron 구현은 두 조건을 AND가 아니라 OR로 평가합니다. 0 0 1 * 1은 "매월 1일이면서 월요일"이 아니라 "매월 1일 또는 매주 월요일"입니다. 한 달에 한 번 돌 줄 알았던 작업이 다섯 번 돕니다.
원래 Vixie cron의 의도된 동작이고 POSIX 규격에도 그렇게 적혀 있습니다. "매월 1일과 매주 금요일에 백업"처럼 두 일정을 한 줄로 쓰려는 용도였습니다. 지금은 대부분 이 동작을 모른 채로 AND를 기대해서 문제가 됩니다.
둘 중 하나는 반드시 *로 두세요. "매월 1일"이면 0 0 1 * *, "매주 월요일"이면 0 0 * * 1입니다. AND가 정말 필요하면 cron은 넓게 걸고 스크립트 첫 줄에서 날짜를 확인해 빠져나오는 방식이 안전합니다. 스케줄러 구현마다 다른 걸 믿는 것보다 코드에 조건을 두는 편이 낫습니다.
Vixie cron, cronie 계열은 OR입니다. 반면 Quartz(Java)나 일부 클라우드 스케줄러는 아예 문법이 달라 요일 칸에 ?를 요구하며 동시 지정을 금지합니다. 그래서 표현식을 다른 런타임으로 옮길 때는 문자열만 복사하지 말고 그 런타임 문서에서 한 번 더 확인해야 합니다.
공식 리눅스 이미지 대부분이 UTC로 뜹니다. 0 9 * * *를 "아침 9시"로 쓰면 한국 기준 오후 6시에 돕니다. 로컬에서 맞던 스케줄이 배포 후 아홉 시간 밀리는 전형적인 경로입니다.
date를 그 환경에서 직접 찍어보는 게 가장 확실합니다. 파일의 TZ=나 CRON_TZ= 지원 여부는 구현마다 다르고, 지원해도 그 줄 이후에만 적용되는 경우가 있습니다.
한국은 서머타임이 없어 이 문제를 피하지만, 미국·유럽 리전에 배포하면 전환일에 특정 시각이 아예 없거나 두 번 옵니다. 새벽 2~3시대 스케줄은 그래서 피하는 게 관행입니다.
표현식이 의도한 시각과 맞는지는 Cron 표현식 파서의 다음 실행 목록으로 확인하고, epoch와 지역 시각을 대조해야 할 때는 Unix 타임스탬프 변환기를 같이 쓰면 편합니다.
문법과 시각이 맞아도 운영에서 깨지는 세 가지입니다. 크롤링이나 정산처럼 주기가 짧고 실행이 긴 작업에서 특히 자주 만납니다.
cron은 이전 프로세스가 살아 있는지 보지 않습니다. */5로 걸어둔 작업이 7분 걸리기 시작하면 계속 겹쳐 쌓이고, DB 커넥션이나 메모리가 먼저 터집니다. flock -n으로 락 파일을 잡거나 스크립트 안에서 PID 파일을 확인해 이미 돌고 있으면 그냥 종료하세요. 재시도보다 건너뛰기가 안전한 경우가 많습니다.
같은 crontab이 들어간 컨테이너를 3개로 스케일하면 작업이 세 번 돕니다. 알림이 세 번 가고 정산이 세 번 반영됩니다. 스케줄러는 인스턴스 수와 무관한 곳(전용 워커 하나, 또는 관리형 스케줄러)에 두고, 그게 어려우면 작업 자체를 멱등하게 만들어야 합니다.
cron은 표준 출력과 오류를 메일로 보내려 하고, 메일이 설정되지 않은 서버에서는 그대로 사라집니다. >> /var/log/작업.log 2>&1로 리다이렉트하지 않으면 실패를 알 방법이 없습니다. 더 나은 방법은 작업 끝에 헬스체크 신호를 보내고, 그 신호가 오지 않을 때 알림이 오게 하는 것입니다. 실행 실패보다 실행되지 않은 것을 알아채기가 훨씬 어렵습니다.
cron은 최소한의 환경으로 실행합니다. 터미널에서 되던 명령이 cron에서 "command not found"가 나는 이유가 대부분 이것입니다. 절대 경로를 쓰거나 스크립트 첫 줄에서 필요한 환경을 직접 불러오세요. 파이썬 가상환경은 특히 자주 걸립니다.
*인지 확인합니다. 둘 다 값이면 OR로 돕니다.date를 찍어 타임존을 확인합니다.이어서 볼 페이지: Cron 표현식 파서로 표현식을 검산하고, Unix 타임스탬프 변환기로 로그의 epoch를 읽고, 날짜 계산기로 주기와 다음 정산일을 맞춥니다.