왜 이 글을 쓰나
cron은 쓸 때마다 찾아본다. "매주 월요일 오전 9시"를 cron으로 어떻게 쓰더라? 0 9 * * 1 맞지? 아니면 0 9 * * MON이었나? 6자리 cron이면 앞에 초(second) 필드가 붙는다는데, 내가 쓰는 라이브러리가 5자리인지 6자리인지도 헷갈린다.
자동화 스크립트 짤 때마다 반복하는 이 검색을 끝내려고 정리했다. 기본 구조부터 자주 쓰는 패턴, Linux crontab 사용법, Node.js 라이브러리까지 한 곳에 모았다.
cron 표현식 기본 구조
5자리 vs 6자리
| 형식 | 필드 구성 | 사용처 |
|---|---|---|
| 5자리 | 분 시 일 월 요일 |
Linux crontab, GitHub Actions |
| 6자리 | 초 분 시 일 월 요일 |
Spring Scheduler, Quartz, AWS EventBridge |
| 7자리 | 초 분 시 일 월 요일 연도 |
Quartz (연도 선택 포함) |
가장 흔히 쓰는 건 5자리다. Linux crontab과 대부분의 온라인 cron 생성기가 5자리 기준이다. Node.js의 node-cron도 5자리, node-schedule은 6자리(초 포함)를 지원한다. 라이브러리 문서를 먼저 확인하는 게 맞다.
각 필드 범위
| 필드 | 범위 | 특이사항 |
|---|---|---|
| 초 (second) | 0–59 | 6자리 이상에서만 |
| 분 (minute) | 0–59 | |
| 시 (hour) | 0–23 | |
| 일 (day of month) | 1–31 | |
| 월 (month) | 1–12 또는 JAN–DEC | |
| 요일 (day of week) | 0–7 또는 SUN–SAT | 0과 7 모두 일요일 |
요일에서 0과 7이 둘 다 일요일인 건 알고 있어야 한다. 0 * * * 0과 0 * * * 7 모두 매주 일요일이다.
특수 문자 정리
| 문자 | 의미 | 예시 |
|---|---|---|
* |
모든 값 | * → 매분, 매시, 매일 |
, |
여러 값 나열 | 1,3,5 → 1, 3, 5번째 |
- |
범위 지정 | 9-18 → 9시부터 18시까지 |
/ |
간격 지정 | */5 → 5단위마다 |
L |
마지막 | 일 필드에서 L → 말일, 요일 필드에서 5L → 마지막 금요일 |
W |
가장 가까운 평일 | 15W → 15일에서 가장 가까운 평일 |
# |
n번째 특정 요일 | 5#2 → 두 번째 금요일 |
L, W, #은 Quartz 등 일부 라이브러리에서만 지원한다. Linux crontab에는 없다.
자주 쓰는 패턴 모음
바로 복붙할 수 있도록 정리했다. 5자리 기준이다.
기본 패턴
| 설명 | cron 표현식 |
|---|---|
| 매분 실행 | * * * * * |
| 매시 정각 | 0 * * * * |
| 매일 자정 (00:00) | 0 0 * * * |
| 매일 오전 6시 | 0 6 * * * |
| 매주 월요일 자정 | 0 0 * * 1 |
| 매월 1일 자정 | 0 0 1 * * |
| 매년 1월 1일 자정 | 0 0 1 1 * |
간격 패턴
| 설명 | cron 표현식 |
|---|---|
| 5분마다 | */5 * * * * |
| 10분마다 | */10 * * * * |
| 30분마다 | */30 * * * * |
| 2시간마다 | 0 */2 * * * |
| 매일 6시간마다 (0, 6, 12, 18시) | 0 */6 * * * |
특정 시간대 패턴
| 설명 | cron 표현식 |
|---|---|
| 평일(월~금)만 자정 | 0 0 * * 1-5 |
| 주말(토~일)만 자정 | 0 0 * * 6,0 |
| 업무 시간(9~18시) 매시 정각 | 0 9-18 * * * |
| 평일 업무 시간 매시 정각 | 0 9-18 * * 1-5 |
| 특정 시각 여러 개 (9시, 13시, 18시) | 0 9,13,18 * * * |
| 매일 오전 9시 30분 | 30 9 * * * |
실전 조합 패턴
| 설명 | cron 표현식 |
|---|---|
| 매주 월요일 오전 9시 | 0 9 * * 1 |
| 매월 마지막 날 자정 (Quartz) | 0 0 L * ? |
| 매주 두 번째 금요일 (Quartz) | 0 0 ? * 5#2 |
| 평일 오전 8시 30분 | 30 8 * * 1-5 |
| 매 15분마다 (0, 15, 30, 45분) | 0,15,30,45 * * * * |
Linux crontab 사용법
기본 명령어
# crontab 편집기 열기
crontab -e
# 현재 등록된 cron 목록 보기
crontab -l
# 모든 cron 삭제 (주의: 되돌릴 수 없음)
crontab -r
# 특정 사용자의 crontab 편집 (root 권한 필요)
crontab -u username -e
crontab 파일 형식
# 분 시 일 월 요일 명령어
* * * * * /path/to/command
# 환경 변수 설정 (상단에 작성)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=admin@example.com # 실행 결과를 이메일로 수신
# 예시
0 2 * * * /home/user/backup.sh # 매일 새벽 2시에 백업
*/5 * * * * /usr/local/bin/health-check.sh # 5분마다 헬스체크
0 9 * * 1 /home/user/weekly-report.sh >> /var/log/report.log 2>&1
출력 리다이렉트
# 표준 출력 + 에러를 파일에 저장
0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1
# 출력 버리기 (로그 불필요할 때)
*/5 * * * * /usr/local/bin/check.sh > /dev/null 2>&1
# 표준 출력만 파일에, 에러는 버리기
0 * * * * /home/user/script.sh >> /var/log/output.log 2>/dev/null
2>&1은 표준 에러(2)를 표준 출력(1)으로 합치는 것이다. 둘 다 같은 파일에 남기고 싶을 때 쓴다.
/etc/crontab vs crontab -e
| 항목 | crontab -e | /etc/crontab |
|---|---|---|
| 대상 | 현재 사용자 | 시스템 전체 |
| 사용자 필드 | 없음 | 있음 (명령어 앞에 사용자 지정) |
| 편집 권한 | 일반 사용자 가능 | root만 |
| 사용 시점 | 개인 스크립트 | 시스템 레벨 작업 |
# /etc/crontab 형식 (사용자 필드 추가됨)
# 분 시 일 월 요일 사용자 명령어
0 2 * * * root /usr/local/bin/backup.sh
Node.js에서 cron
node-cron (5자리)
npm install node-cron
const cron = require('node-cron');
// 매분 실행
cron.schedule('* * * * *', () => {
console.log('1분마다 실행');
});
// 매일 자정 DB 백업
cron.schedule('0 0 * * *', async () => {
console.log('DB 백업 시작:', new Date().toISOString());
await backupDatabase();
});
// 평일 오전 9시 리포트 발송
cron.schedule('0 9 * * 1-5', async () => {
await sendDailyReport();
}, {
timezone: 'Asia/Seoul' // 타임존 지정 (중요!)
});
// cron 작업 제어
const task = cron.schedule('*/30 * * * *', () => {
checkServerHealth();
}, {
scheduled: false // 즉시 시작하지 않음
});
task.start(); // 수동 시작
task.stop(); // 중지
task.destroy(); // 제거
node-schedule (6자리 + 더 유연한 규칙)
npm install node-schedule
const schedule = require('node-schedule');
// 6자리 cron (초 분 시 일 월 요일)
const job = schedule.scheduleJob('0 0 0 * * *', () => {
console.log('매일 자정 (초 포함 6자리)');
});
// 날짜 객체로 특정 시각 지정
const date = new Date(2026, 11, 25, 9, 0, 0); // 크리스마스 오전 9시
const christmasJob = schedule.scheduleJob(date, () => {
console.log('메리 크리스마스!');
});
// 규칙 객체로 더 세밀하게
const rule = new schedule.RecurrenceRule();
rule.dayOfWeek = [1, 2, 3, 4, 5]; // 월~금
rule.hour = 9;
rule.minute = 0;
rule.tz = 'Asia/Seoul';
schedule.scheduleJob(rule, () => {
sendMorningBriefing();
});
// 작업 취소
job.cancel();
언어별 cron 라이브러리 비교
| 언어 | 라이브러리 | cron 자리 | 타임존 지원 | 특징 |
|---|---|---|---|---|
| Node.js | node-cron | 5자리 | ✓ (옵션) | 가볍고 단순, 가장 많이 쓰임 |
| Node.js | node-schedule | 5/6자리 | ✓ | 날짜 객체, 규칙 객체 지원 |
| Node.js | cron (npm) | 6자리 | ✓ | 초 단위 스케줄링 |
| Python | APScheduler | 6자리 | ✓ | cron/interval/date 3가지 방식 |
| Python | schedule | 자체 DSL | X (별도 설정) | 간단한 자체 문법 (every(5).minutes.do(...)) |
| Java | Spring Scheduler | 6자리 | ✓ | @Scheduled 어노테이션 |
| Java | Quartz | 7자리 | ✓ | 엔터프라이즈급, L/W/# 지원 |
| PHP | Laravel Task Scheduling | 5자리 | ✓ | ->cron(), ->everyFiveMinutes() 체이닝 |
| PHP | cron-expression | 5자리 | X | 파서만 제공 |
5자리 vs 6자리, crontab vs 라이브러리 비교
5자리 vs 6자리
| 항목 | 5자리 (분 시 일 월 요일) | 6자리 (초 분 시 일 월 요일) |
|---|---|---|
| 최소 단위 | 1분 | 1초 |
| 사용처 | Linux crontab, GitHub Actions, node-cron | Spring, Quartz, node-schedule |
| 초 단위 스케줄링 | 불가 | 가능 |
| 가독성 | 높음 | 상대적으로 낮음 (초 필드 앞에 붙어 혼동) |
| 온라인 생성기 호환 | 대부분 호환 | 라이브러리마다 다름 |
Linux crontab vs 애플리케이션 내장 스케줄러
| 항목 | Linux crontab | 애플리케이션 스케줄러 |
|---|---|---|
| 설정 방식 | 시스템 파일/명령어 | 코드 내 설정 |
| 의존성 | OS 수준 | 라이브러리 |
| 서버 재시작 후 지속 | ✓ (자동) | ✓ (앱 재시작 필요) |
| 타임존 제어 | 서버 타임존 따름 | 코드에서 직접 설정 |
| 로그 관리 | 별도 설정 필요 | 앱 로깅 시스템 활용 |
| 분산 환경 | 단일 서버 기준 | 락 메커니즘 필요 |
| 적합한 경우 | 단순 쉘 스크립트, OS 유지보수 | 앱 로직과 연계된 작업 |
실전 활용 예시
DB 백업 자동화
# Linux crontab: 매일 새벽 2시 MySQL 백업
0 2 * * * /home/ubuntu/scripts/backup-db.sh >> /var/log/db-backup.log 2>&1
#!/bin/bash
# backup-db.sh
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/home/ubuntu/backups"
DB_NAME="myapp"
mkdir -p $BACKUP_DIR
mysqldump -u root -p"$DB_PASSWORD" $DB_NAME | gzip > "$BACKUP_DIR/backup_$DATE.sql.gz"
# 7일 이상 된 백업 삭제
find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete
echo "[$DATE] 백업 완료: backup_$DATE.sql.gz"
캐시 초기화
// Node.js: 매시 정각 Redis 캐시 초기화
const cron = require('node-cron');
const redis = require('redis');
const client = redis.createClient();
cron.schedule('0 * * * *', async () => {
const keys = await client.keys('cache:product:*');
if (keys.length > 0) {
await client.del(keys);
console.log(`[${new Date().toISOString()}] 캐시 ${keys.length}개 초기화`);
}
}, {
timezone: 'Asia/Seoul'
});
리포트 이메일 발송
// 매주 월요일 오전 9시 주간 리포트 발송
cron.schedule('0 9 * * 1', async () => {
const report = await generateWeeklyReport();
await sendEmail({
to: 'team@example.com',
subject: `[주간 리포트] ${new Date().toLocaleDateString('ko-KR')}`,
html: report,
});
console.log('주간 리포트 발송 완료');
}, {
timezone: 'Asia/Seoul'
});
삽질 기록
1. 타임존 문제 — 서버는 UTC, 나는 KST
가장 많이 당하는 문제다. 서버 타임존이 UTC로 설정된 경우, 0 9 * * *은 한국 시각 오전 9시가 아니라 오전 6시 (UTC+9 기준 오후 6시) 에 실행된다. 서버 타임존이 UTC면 KST로 맞추려면 9시간 빼야 한다.
// 잘못된 설정 (서버 UTC, 의도는 KST 오전 9시)
cron.schedule('0 9 * * *', task); // 실제로는 UTC 기준 9시 = KST 오후 6시
// 올바른 설정 1: timezone 옵션 사용
cron.schedule('0 9 * * *', task, { timezone: 'Asia/Seoul' });
// 올바른 설정 2: UTC로 계산해서 표기
cron.schedule('0 0 * * *', task); // UTC 0시 = KST 9시
배포 환경마다 서버 타임존이 다를 수 있다. cron 라이브러리의 timezone 옵션을 항상 명시적으로 설정하는 게 안전하다.
2. *와 */1의 차이
# 이 둘은 동일하다
* * * * * # 매분
*/1 * * * * # 1분마다 = 매분
*/1은 "1씩 증가하는 모든 값"이라서 *와 완전히 같다. 헷갈리는 경우가 있어서 기록해둔다. */2는 0, 2, 4... 짝수 분에 실행, */5는 0, 5, 10, 15... 5의 배수 분에 실행된다.
3. 서버 재시작 후 cron이 날아감
crontab -e로 등록한 cron은 서버 재시작 이후에도 살아있다. 문제는 Node.js 앱 내부에 node-cron으로 등록한 작업이다. 앱이 내려가면 cron도 같이 내려간다. 앱이 pm2나 systemd로 관리되지 않으면 서버 재시작 후 cron이 실행되지 않는다.
# pm2로 앱 관리 + 서버 재시작 후 자동 재개
pm2 start app.js --name "myapp"
pm2 startup # 서버 재시작 후 pm2 자동 시작 설정
pm2 save # 현재 프로세스 목록 저장
앱 내 cron을 쓴다면 프로세스 매니저 설정이 필수다.
4. 분산 환경에서 cron 중복 실행
서버 인스턴스가 여러 대인 환경에서 각 서버에 동일한 cron이 등록되면 작업이 중복으로 실행된다. DB 집계나 이메일 발송 같은 건 두 번 실행되면 안 된다.
해결 방법은 여러 가지다.
// Redis 분산 락 사용 (node-cron + redis)
const cron = require('node-cron');
const redis = require('redis');
const client = redis.createClient();
cron.schedule('0 9 * * 1', async () => {
const lockKey = 'cron:weekly-report';
const lockAcquired = await client.set(lockKey, '1', {
NX: true, // 키가 없을 때만 설정
EX: 3600, // 1시간 후 자동 해제
});
if (!lockAcquired) {
console.log('다른 서버에서 이미 실행 중');
return;
}
try {
await sendWeeklyReport();
} finally {
await client.del(lockKey);
}
}, { timezone: 'Asia/Seoul' });
단순하게 해결하려면 cron을 특정 서버(또는 전용 worker 서버) 하나에서만 실행하는 방법도 있다.
마무리
cron 표현식 자체는 어렵지 않다. 5자리 기준으로 분 시 일 월 요일을 외우고, *는 전체, /는 간격, ,는 나열, -는 범위라는 것만 기억하면 대부분의 케이스를 커버한다.
실전에서 자주 만나는 함정은 표현식 문법보다 타임존, 라이브러리별 자리수 차이, 분산 환경 중복 실행이다. 특히 타임존은 한 번이라도 틀려보면 잊어버리지 않는다.
crontab guru에서 표현식을 입력하면 실행 일정을 텍스트로 설명해준다. 작성한 표현식이 맞는지 확인할 때 쓰기 좋다.
기술 스택: cron, crontab, node-cron, node-schedule, Linux, Redis
참고 도구: crontab guru, Cron Expression Generator
'개발 팁' 카테고리의 다른 글
| SSH 터널링 & 포트 포워딩 — 원격 DB 접속부터 내부망 우회까지 (1) | 2026.03.20 |
|---|---|
| TypeScript 실전 팁 — 유틸리티 타입, 타입 가드, 제네릭 완전 정복 (0) | 2026.03.20 |
| npm vs yarn vs pnpm vs Bun — 패키지 매니저 제대로 선택하기 (1) | 2026.03.20 |
| Nginx 실전 설정 — 리버스 프록시, HTTPS, 캐싱 완전 정복 (0) | 2026.03.20 |
| 웹 성능 최적화 기초 — Lighthouse 점수 올리는 실전 방법 (0) | 2026.03.19 |