Cron 표현식 완벽 가이드 — 스케줄 자동화의 기본
Cron 표현식 문법, 예제, 자주 쓰는 패턴을 정리했다. 리눅스 crontab부터 GitHub Actions까지.

서버에서 매일 새벽 3시에 DB 백업을 돌리거나, 매시간 캐시를 갱신하거나, 매주 월요일에 리포트를 보내야 할 때. 이런 반복 작업을 예약하는 데 cron이 쓰인다. 리눅스 시스템 관리의 기본이면서, GitHub Actions, Kubernetes CronJob, AWS EventBridge 등에서도 동일한 표현식을 사용한다.
문법 자체는 간단한데, 플랫폼마다 미묘하게 다른 부분이 있어서 한 번 정리해두면 두고두고 쓸 수 있다.
Cron 표현식 구조
기본 형태는 5개 필드다:
┌───────────── 분 (0-59)
│ ┌───────────── 시 (0-23)
│ │ ┌───────────── 일 (1-31)
│ │ │ ┌───────────── 월 (1-12)
│ │ │ │ ┌───────────── 요일 (0-6, 0=일요일)
│ │ │ │ │
* * * * *
각 필드에 올 수 있는 값:
| 기호 | 의미 | 예시 |
|---|---|---|
* | 모든 값 | * * * * * = 매분 |
| 숫자 | 특정 값 | 30 9 * * * = 매일 9시 30분 |
, | 여러 값 | 0,30 * * * * = 매시 0분과 30분 |
- | 범위 | 1-5 = 1~5 |
/ | 간격 | */10 * * * * = 10분마다 |
자주 쓰는 패턴
시간 기반
* * * * * 매분
*/5 * * * * 5분마다
0 * * * * 매시 정각
0 */2 * * * 2시간마다
30 9 * * * 매일 오전 9시 30분
0 0 * * * 매일 자정
0 9,18 * * * 매일 오전 9시, 오후 6시
요일 기반
0 9 * * 1 매주 월요일 오전 9시
0 9 * * 1-5 평일 오전 9시
0 0 * * 0 매주 일요일 자정
0 18 * * 5 매주 금요일 오후 6시
날짜 기반
0 0 1 * * 매월 1일 자정
0 0 1,15 * * 매월 1일, 15일 자정
0 9 1 1 * 매년 1월 1일 오전 9시
0 0 * * 1#1 매월 첫 번째 월요일 (일부 시스템)
crontab 사용법
리눅스에서 cron 작업을 관리하려면 crontab 명령어를 쓴다.
# 현재 사용자의 cron 작업 목록 보기
crontab -l
# cron 작업 편집
crontab -e
# 특정 사용자의 cron 작업 보기 (root 권한 필요)
sudo crontab -u www-data -l
crontab -e로 열리는 파일에 한 줄에 하나씩 작성한다:
# 매일 새벽 3시에 DB 백업
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
# 10분마다 헬스체크
*/10 * * * * curl -s https://mysite.com/health > /dev/null
>> /var/log/backup.log 2>&1 부분은 출력과 에러를 로그 파일에 기록하라는 뜻이다. 이걸 안 하면 cron이 보내는 메일로 출력이 갈 수 있다.
주의할 점
시간대(timezone). 시스템 시간대를 확인해야 한다. UTC로 설정된 서버에서 0 9 * * *은 한국 시간 오후 6시가 된다.
timedatectl # 현재 시스템 시간대 확인
환경변수. cron은 사용자의 쉘 환경을 그대로 로드하지 않는다. PATH, HOME 등이 다를 수 있어서, 스크립트에서 절대 경로를 쓰는 게 안전하다.
# crontab 파일 상단에 환경변수 지정 가능
PATH=/usr/local/bin:/usr/bin:/bin
겹침 방지. 작업이 실행 시간보다 오래 걸리면 다음 실행과 겹칠 수 있다. flock으로 잠금을 거는 게 일반적인 해결책이다:
*/5 * * * * flock -n /tmp/myjob.lock /home/user/scripts/slow-job.sh
서머타임(DST). 시스템 시간대가 DST를 적용하는 지역이라면, 새벽 2시 30분에 설정한 작업이 봄에는 건너뛰어지고 가을에는 두 번 실행될 수 있다. 중요한 작업은 UTC 기준으로 설정하면 이 문제를 피할 수 있다.
로그 관리. cron 작업이 많아지면 로그를 한 곳에 모아서 관리하는 게 편하다. 각 작업별로 로그 파일을 분리하고, logrotate로 오래된 로그를 자동 정리하면 디스크 용량 걱정도 줄어든다.
# logrotate 설정 예시 (/etc/logrotate.d/cron-jobs)
/var/log/cron-jobs/*.log {
weekly
rotate 4
compress
missingok
}
5필드 vs 6필드 vs 7필드
cron 표현식이라고 다 같은 건 아니다. 플랫폼에 따라 필드 수가 다르다:
| 필드 수 | 구성 | 사용처 |
|---|---|---|
| 5필드 | 분 시 일 월 요일 | 리눅스 crontab, GitHub Actions |
| 6필드 | 초 분 시 일 월 요일 | Spring @Scheduled, Quartz |
| 6필드 (변형) | 분 시 일 월 요일 연도 | AWS EventBridge |
Spring Boot에서 @Scheduled(cron = "0 0 9 * * MON")을 쓸 때, 첫 번째 필드가 초라는 걸 모르면 헷갈린다. 리눅스 crontab 기준으로 작성했다가 초 필드를 빠뜨리는 실수가 흔하다.
GitHub Actions에서 cron
GitHub Actions의 schedule 트리거도 cron 표현식을 쓴다.
on:
schedule:
- cron: '0 9 * * 1' # 매주 월요일 9시 (UTC)
주의: GitHub Actions의 시간대는 항상 UTC이고, 정확한 시각에 실행된다는 보장이 없다. 부하가 많을 때는 몇 분~수십 분 딜레이가 발생할 수 있다.
Kubernetes CronJob
쿠버네티스에서도 동일한 표현식을 사용한다:
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: my-backup-image
restartPolicy: OnFailure
concurrencyPolicy 설정으로 이전 작업이 아직 실행 중일 때의 동작을 제어할 수 있다: Allow(동시 실행 허용), Forbid(새 작업 건너뜀), Replace(이전 작업 종료 후 새로 시작).
AWS EventBridge (CloudWatch Events)
AWS는 cron 표현식에 6개 필드를 쓴다 (연도 추가):
cron(분 시 일 월 요일 연도)
cron(0 9 ? * MON-FRI *) # 평일 9시 UTC
?는 "지정 안 함"이라는 뜻으로, 일과 요일 중 하나에 반드시 써야 한다. 표준 cron과 살짝 문법이 다르니 AWS 문서를 꼭 참고해야 한다.
디버깅
cron 작업이 안 돌 때 확인할 것들:
# cron 서비스가 돌고 있는지
systemctl status cron
# cron 실행 로그
grep CRON /var/log/syslog
# 스크립트 권한 확인 (실행 권한 있어야 함)
chmod +x /home/user/scripts/backup.sh
권한 문제가 제일 많다. 스크립트를 직접 실행하면 되는데 cron에서만 안 되는 경우, 대부분 PATH나 파일 권한 문제다.
표현식이 내가 원하는 대로 동작하는지 미리 확인하고 싶으면 Cron 표현식 생성기를 써보자. 각 필드를 GUI로 설정하면 다음 실행 시간을 미리 보여준다. */15 3-6 * * 1-5 같은 복잡한 표현식도 한눈에 확인할 수 있다.
실전에서 자주 하는 실수
"매월 31일"에 걸면 안 된다. 0 0 31 * *은 2월, 4월, 6월 등 31일이 없는 달에는 아예 실행되지 않는다. 월말 작업이 필요하면 1일 자정에 실행하고 스크립트 안에서 전날(월말) 데이터를 처리하는 게 안전하다.
0 */2 * * *과 0 0-23/2 * * *은 다르다. 사실 결과는 같은데, 전자가 훨씬 읽기 쉽다. 가독성 면에서 / 문법을 쓰는 게 팀 협업에 유리하다.
일(day of month)과 요일(day of week)을 동시에 지정하면? 표준 cron에서는 OR 조건으로 동작한다. 0 9 15 * 1은 "15일 또는 월요일"에 실행된다. AND 조건이라고 착각하기 쉬운 부분이다. AWS EventBridge에서는 ? 기호를 강제해서 이 혼동을 원천 차단하고 있다.
systemd timer — cron의 대안
최근 리눅스 배포판에서는 cron 대신 systemd timer를 쓰는 경우도 많다. cron 대비 몇 가지 장점이 있다:
- 실행 로그가
journalctl로 통합 관리된다 - 서비스 의존성을 지정할 수 있다 (네트워크 연결 이후 실행 등)
OnBootSec,OnCalendar같은 유연한 스케줄 옵션
# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true로 설정하면, 서버가 꺼져있었던 시간대의 놓친 실행을 부팅 후 자동으로 보충한다. cron에는 이런 기능이 없어서 서버 재시작 후 한 번 놓치면 그냥 지나가버린다.
cron이 더 간단하고 범용적이라 완전히 대체되지는 않겠지만, systemd 기반 서버에서는 timer도 고려해볼 만하다.