~ / posts / developer
smith@lab:~/posts$ cat developer/cron-expression-guide.md

크론 표현식 읽는 법, 5필드 문법과 예제 20개·타임존 함정 정리

0 9 * * 1-5 를 보고 "평일 오전 9시"가 바로 읽히면 크론은 다 아는 거예요. 그런데 막상 직접 쓰려고 하면 "요일 0이 일요일이었나 월요일이었나", "*/5 는 5분 뒤부터인가 정각부터인가" 같은 게 헷갈리고, 배포하고 나서야 아침 9시 알림이 저녁 6시에 오더라고요. 결론부터 말하면 크론 표현식은 분 시 일 월 요일 다섯 칸이고, 각 칸에 *(전부), ,(목록), -(범위), /(간격)를 조합해 써요. 그리고 실수의 대부분은 문법이 아니라 타임존(서버는 UTC, 나는 KST)과 "일·요일을 같이 쓰면 OR" 규칙에서 나와요.

이 글에서는 다섯 필드를 읽는 법, 특수 문자의 정확한 의미, 날짜와 요일 OR 규칙의 함정, UTC·KST 변환, 바로 쓰는 예제 20개, 크론 환경에서 자주 겪는 실수, 그리고 리눅스 crontab·systemd timer·GitHub Actions·쿠버네티스 CronJob이 어떻게 다른지 정리할게요. 표현식을 넣고 다음 실행 시각을 바로 확인하고 싶다면 크론 표현식 해석기를 같이 열어 두세요.

핵심 요약

크론은 분(0-59) 시(0-23) 일(1-31) 월(1-12) 요일(0-7) 순서예요. 요일 0과 7은 둘 다 일요일이에요.

*/15 는 0분부터 15분 간격(0·15·30·45분), 1-5 는 범위, 1,15 는 목록이에요.

일과 요일을 둘 다 지정하면 AND가 아니라 OR이에요. 0 9 13 * 5 는 "13일의 금요일"이 아니라 "매월 13일 또는 매주 금요일"이라 2026년에 61번 실행돼요.

서버·컨테이너·GitHub Actions는 보통 UTC예요. KST 오전 9시는 UTC 0시라서 0 0 * * * 로 적어야 해요.

크론 표현식 5필드 읽는 법

리눅스 crontab 한 줄은 시간 5칸과 실행할 명령으로 이뤄져요.

┌───────── 분 (0-59)
│ ┌─────── 시 (0-23)
│ │ ┌───── 일 (1-31)
│ │ │ ┌─── 월 (1-12 또는 JAN-DEC)
│ │ │ │ ┌─ 요일 (0-7 또는 SUN-SAT, 0과 7은 일요일)
│ │ │ │ │
30 9 * * 1-5  /home/smith/report.sh

읽을 때는 오른쪽에서 왼쪽으로, 큰 단위부터 읽으면 자연스러워요. 위 예는 "요일은 월~금, 월은 아무 때나, 일은 아무 날이나, 9시 30분"이니까 "평일 오전 9시 30분"이에요. 시는 24시간제라서 오후 6시는 18로 적고, 분 칸을 * 로 두면 그 시간 동안 매분 실행된다는 점을 꼭 기억하세요. * 9 * * * 는 9시에 한 번이 아니라 9:00부터 9:59까지 60번이에요.

크론 표현식 30 9 * * 1-5를 분·시·일·월·요일 다섯 칸으로 나누고 각 칸의 허용 범위와 의미를 풀어 평일 오전 9시 30분이 되는 과정을 보여 주는 그림
크론은 분 시 일 월 요일 순서, 오른쪽 큰 단위부터 읽으면 쉬워요

특수 문자: *, */n, 범위, 목록

기호의미예해석
*모든 값시 칸 *매시
,목록분 칸 0,300분과 30분
-범위 (양 끝 포함)시 칸 9-189시부터 18시까지 10번
/간격분 칸 */150·15·30·45분
a-b/n범위 안에서 간격분 칸 5-59/105·15·25·35·45·55분

*/n은 "n분마다"가 아니라 "0에서 시작해 n의 배수"예요

*/15 는 "지금부터 15분 뒤마다"가 아니라 0분부터 시작해 15씩 더한 값이에요. 그래서 크론을 10시 7분에 등록해도 첫 실행은 10시 15분이에요. 60을 나누어떨어지게 하는 값(1·2·3·4·5·6·10·12·15·20·30)이면 문제가 없는데, 그렇지 않으면 간격이 고르지 않아요.

예를 들어 */7 은 0·7·14·21·28·35·42·49·56분에 실행되고, 다음은 다시 정각 0분이라 56분 → 0분 사이만 4분 간격이 돼요. 시 칸의 */5 도 0·5·10·15·20시 다음 0시로 넘어가서 20시 → 0시는 4시간 간격이에요. 정확히 7분마다가 필요하면 크론 대신 systemd timer나 앱 안의 스케줄러를 쓰는 게 맞아요.

간격 표현식별 하루 실행 횟수
* * * * * (매분)* * * * * (매분) · 1,440회1,440회*/5 * * * **/5 * * * * · 288회288회*/10 * * * **/10 * * * * · 144회144회*/15 * * * **/15 * * * * · 96회96회*/30 * * * **/30 * * * * · 48회48회0 * * * * (매시)0 * * * * (매시) · 24회24회

매분 실행은 하루 1440번이에요. 외부 API를 부르는 작업이라면 호출 한도부터 확인하세요.

단위: 회 · 자료: 하루 1440분을 간격으로 나눠 직접 계산
표로 보기
구분하루 실행
* * * * * (매분)1,440
*/5 * * * *288
*/10 * * * *144
*/15 * * * *96
*/30 * * * *48
0 * * * * (매시)24

숫자 대신 이름도 쓸 수 있어요

월과 요일 칸에는 JAN~DEC, SUN~SAT 같은 영문 약어를 쓸 수 있어요(대소문자 무관). 0 9 * * MON-FRI 가 0 9 * * 1-5 보다 읽기 쉬워서 저는 요일은 이름으로 적는 편이에요. 또 대부분의 구현이 @hourly(= 0 * * * *), @daily(= 0 0 * * *), @weekly(= 0 0 * * 0), @monthly(= 0 0 1 * *), @yearly(= 0 0 1 1 *), @reboot(부팅 시 한 번) 같은 단축어를 지원해요.

일과 요일을 같이 쓰면 OR, 가장 흔한 함정

크론에서 제일 많이 당하는 함정이에요. 표준 크론(Vixie cron, cronie, POSIX)에서는 일(3번째 칸)과 요일(5번째 칸)이 둘 다 * 가 아니면 둘 중 하나만 맞아도 실행해요. 다른 칸들은 전부 AND인데 이 둘만 OR이에요.

그래서 "13일의 금요일 오전 9시"를 0 9 13 * 5 로 적으면 실제로는 "매월 13일 오전 9시, 그리고 매주 금요일 오전 9시"가 돼요. 2026년 달력으로 실제 실행 횟수를 세어 보면 차이가 확 보여요.

2026년 월별 실행 횟수 — 0 9 13 * 5 의 실제 동작 vs 의도
실제 (13일 또는 금요일)의도 (13일의 금요일)
01234561월 · 실제 (13일 또는 금요일) 6회1월 · 의도 (13일의 금요일) 0회1월2월 · 실제 (13일 또는 금요일) 4회2월 · 의도 (13일의 금요일) 1회2월3월 · 실제 (13일 또는 금요일) 4회3월 · 의도 (13일의 금요일) 1회3월4월 · 실제 (13일 또는 금요일) 5회4월 · 의도 (13일의 금요일) 0회4월5월 · 실제 (13일 또는 금요일) 6회5월 · 의도 (13일의 금요일) 0회5월6월 · 실제 (13일 또는 금요일) 5회6월 · 의도 (13일의 금요일) 0회6월7월 · 실제 (13일 또는 금요일) 6회7월 · 의도 (13일의 금요일) 0회7월8월 · 실제 (13일 또는 금요일) 5회8월 · 의도 (13일의 금요일) 0회8월9월 · 실제 (13일 또는 금요일) 5회9월 · 의도 (13일의 금요일) 0회9월10월 · 실제 (13일 또는 금요일) 6회10월 · 의도 (13일의 금요일) 0회10월11월 · 실제 (13일 또는 금요일) 4회11월 · 의도 (13일의 금요일) 1회11월12월 · 실제 (13일 또는 금요일) 5회12월 · 의도 (13일의 금요일) 0회12월

의도는 1년에 3번(2월·3월·11월 13일)인데 실제로는 61번 실행돼요.

단위: 회 · 자료: 2026년 달력으로 직접 계산 (파이썬 datetime)
표로 보기
구분실제 (13일 또는 금요일)의도 (13일의 금요일)
1월60
2월41
3월41
4월50
5월60
6월50
7월60
8월50
9월50
10월60
11월41
12월50

"매월 마지막 금요일"도 같은 함정에 빠져요. 0 9 25-31 * 5 는 25~31일 매일 + 매주 금요일이에요. 이럴 땐 크론은 날짜만 걸고 요일 판단은 명령 쪽에서 해요.

# 매월 25~31일 9시에 실행하되, 금요일일 때만 실제 작업
0 9 25-31 * * [ "$(date +\%u)" = 5 ] && /home/smith/monthly.sh

여기서 \% 를 눈여겨보세요. crontab에서 % 는 줄바꿈으로 해석돼서 반드시 역슬래시로 이스케이프해야 해요. 이걸 모르고 date +%u 를 그대로 쓰면 명령이 조용히 잘려요.

참고로 Quartz나 스프링 같은 자바 스케줄러는 이 혼란을 피하려고 일과 요일 중 하나에 ? 를 쓰게 하고, 마지막 날 L, 몇째 주 요일 5#3(셋째 금요일) 같은 기호를 지원해요. 리눅스 crontab에는 이런 기호가 없어요.

크론 타임존 실수: 서버 UTC vs 한국 시간 KST

크론은 그 크론이 도는 곳의 시간대로 해석돼요. 그런데 클라우드 서버 이미지, 도커 컨테이너, GitHub Actions는 대부분 UTC예요. 한국은 UTC+9이고 서머타임이 없어서 변환은 늘 "9시간 빼기"예요.

원하는 시각 (KST)UTC로 적을 표현식주의
매일 오전 9시0 0 * * *—
매일 오후 6시0 9 * * *—
매일 자정 0시0 15 * * *UTC로는 전날 15시
평일 오전 8시0 23 * * 0-4요일도 하루 당겨짐 (일~목)
매월 1일 오전 9시0 0 1 * *—
매월 1일 자정표현 불가UTC로는 전달 말일 15시라 28~31일이 바뀜

KST 오전 9시 이전 시각은 UTC로 전날이 돼서 요일과 날짜까지 하루씩 당겨야 해요. 평일 오전 8시를 0 23 * * 1-5 로 적으면 월~금 23시 UTC, 즉 화~토 오전 8시 KST에 돌아요. 특히 "매월 1일 자정(KST)"은 UTC로 "전달 마지막 날 15시"라서 표준 5필드로는 깔끔하게 적을 수 없어요.

UTC 00시와 15시가 각각 KST 09시와 다음 날 00시에 대응해 KST 오전 9시 이전 작업은 UTC 기준 날짜와 요일이 하루 앞당겨지는 관계를 보여 주는 타임라인
KST 0~9시는 UTC로 전날이라 날짜와 요일도 하루 당겨야 해요

해결 방법은 세 가지예요.

  1. 서버 시간대를 확인하고 UTC로 적기. timedatectl 이나 date 로 먼저 확인하고, crontab 맨 위에 # 모든 시각은 UTC 주석을 남겨 두세요.
  2. 크론에 시간대 지정하기. cronie는 crontab 안에 CRON_TZ=Asia/Seoul 을, 쿠버네티스 CronJob은 spec.timeZone 을, systemd timer는 OnCalendar=*-*-* 09:00 Asia/Seoul 처럼 끝에 시간대를 붙일 수 있어요.
  3. 서버 시간대를 KST로 바꾸기. 가능은 하지만 로그·DB 시각까지 같이 바뀌어서 저는 권하지 않아요. 시간 저장은 UTC가 원칙인 이유는 타임존 처리 원칙 글에 정리했어요.

바로 쓰는 크론 표현식 예제 20개

#표현식의미
1* * * * *매분
2*/5 * * * *5분마다 (0·5·10…55분)
3*/15 * * * *15분마다
40 * * * *매시 정각
530 * * * *매시 30분
60 */2 * * *2시간마다 정각 (0·2·4…22시)
70 9 * * *매일 오전 9시
830 18 * * *매일 오후 6시 30분
90 9,18 * * *매일 오전 9시와 오후 6시
100 9-18 * * *매일 9시~18시 매 정각 (10회)
11*/10 9-17 * * 1-5평일 9:00~17:50 10분마다
120 9 * * 1-5평일 오전 9시
130 10 * * 6,0주말 오전 10시
140 9 * * 1매주 월요일 오전 9시
150 0 * * 0매주 일요일 자정
160 0 1 * *매월 1일 자정
170 0 1,15 * *매월 1일과 15일 자정
180 0 1 */3 *분기 첫날 (1·4·7·10월 1일)
190 0 1 1 *매년 1월 1일 자정
2030 3 * * *매일 새벽 3시 30분 (백업 단골)

모두 크론이 도는 곳의 시간대 기준이에요. 서버가 UTC라면 위 표의 시각에 9시간을 더한 게 한국 시간이에요. 11번처럼 "17:50까지"가 되는 이유는 시 칸 9-17이 17시대(17:00~17:59)까지 포함하고, 그 안에서 10분 간격이라 17:50이 마지막이라서예요.

"매월 말일"은 일 칸에 넣을 값이 없어서 이렇게 해요. 0 23 28-31 * * 로 28~31일에 돌리고, 명령 앞에 [ "$(date -d tomorrow +\%d)" = 01 ] && 를 붙여 내일이 1일일 때만 실행하는 방식이에요(GNU date 기준).

크론이 안 돌 때 확인할 것

표현식은 맞는데 작업이 안 돌거나 이상하게 돌 때는 대개 환경 문제예요.

systemd timer·GitHub Actions·k8s CronJob 차이

같은 "정해진 시각에 실행"이라도 도구마다 문법과 동작이 조금씩 달라요.

항목crontabsystemd timerGitHub Actions쿠버네티스 CronJob
문법5필드OnCalendar= 고유 문법5필드5필드
시간대시스템 시간대시스템 (끝에 지정 가능)UTC컨트롤러 시간대, spec.timeZone 지정 가능
놓친 실행버림Persistent=true 면 부팅 후 실행버림startingDeadlineSeconds 안이면 실행
중복 실행막지 않음이미 실행 중이면 새로 안 띄움별도 설정concurrencyPolicy 로 제어
로그직접 리다이렉트journalctlActions 실행 기록Pod 로그

systemd timer

.timer 와 .service 두 파일로 구성해요. OnCalendar=Mon..Fri *-*-* 09:00:00 이 평일 9시, OnCalendar=*:0/5 가 5분마다예요. 문법이 헷갈리면 systemd-analyze calendar "Mon..Fri 09:00" 로 다음 실행 시각을 바로 확인할 수 있어요. 기본 AccuracySec 가 1분이라 실행이 최대 1분 늦을 수 있고, 정확해야 하면 AccuracySec=1s 로 줄이세요.

GitHub Actions schedule

on.schedule.cron 에 5필드를 적고 UTC로 해석돼요. 최소 간격은 5분이고, 사용량이 몰리는 매시 정각에는 실행이 수십 분 늦거나 빠질 수 있어서 17 * * * * 처럼 정각을 피하라고 공식 문서가 권해요. 스케줄은 기본 브랜치의 최신 커밋 기준으로만 돌고, 공개 저장소는 60일 동안 활동이 없으면 예약 워크플로가 자동으로 꺼져요.

쿠버네티스 CronJob

spec.schedule 에 5필드를 쓰고, 1.27부터 정식 기능이 된 spec.timeZone: "Asia/Seoul" 로 시간대를 정할 수 있어요. 표현식 안에 CRON_TZ= 를 넣는 방식은 공식 지원이 아니니 쓰지 마세요. concurrencyPolicy 는 기본값 Allow 라 겹쳐 실행되니, 배치 작업이면 Forbid 로 바꾸는 게 안전해요.

자주 묻는 질문

크론에서 요일 0은 무슨 요일인가요?

일요일이에요. 7도 일요일이고, 1이 월요일, 6이 토요일이에요. 헷갈리면 SUN, MON 처럼 영문 약어를 쓰면 실수가 줄어요.

30초마다 실행하는 크론 표현식이 있나요?

표준 5필드 크론의 최소 단위는 1분이라 초 단위는 안 돼요. 꼭 필요하면 * * * * * 명령 과 * * * * * sleep 30; 명령 두 줄을 등록하거나, 초 단위를 지원하는 systemd timer나 앱 내부 스케줄러를 쓰세요.

*/5와 0/5는 같은가요?

리눅스 crontab에서는 0-59/5 로 써야 하고 0/5 는 구현에 따라 오류가 나거나 다르게 해석될 수 있어요. Quartz 같은 자바 스케줄러에서는 0/5 가 "0부터 5 간격"이라 */5 와 같아요. 이식성을 생각하면 */5 를 쓰세요.

크론 표현식이 6칸이나 7칸인 건 뭔가요?

스프링 @Scheduled 와 Quartz는 맨 앞에 초 칸이 붙은 6칸(Quartz는 연도까지 7칸), AWS EventBridge는 맨 뒤에 연도가 붙은 6칸이에요. 칸 수가 다르면 같은 문자열이 완전히 다른 뜻이 되니, 어느 도구용인지 먼저 확인하세요.

crontab을 수정했는데 바로 적용되나요?

crontab -e 로 저장하면 크론 데몬이 변경을 감지해 다음 분부터 적용해요. 따로 재시작할 필요는 없어요. 다만 /etc/cron.d/ 파일은 줄 끝 줄바꿈과 사용자 이름 칸이 필요하니 형식을 확인하세요.

정리

크론 표현식은 분 시 일 월 요일 다섯 칸이고, *·목록·범위·/n 간격만 알면 대부분 적을 수 있어요. */n 은 0부터 시작하는 배수라 60을 나누어떨어지지 않으면 간격이 고르지 않고, 일과 요일을 둘 다 지정하면 OR로 동작해요. 가장 흔한 사고는 타임존이라, 서버·컨테이너·GitHub Actions가 UTC인지 먼저 확인하고 KST에서 9시간을 빼서 적으세요.

표현식을 넣으면 사람이 읽는 설명과 다음 실행 시각을 보여 주는 크론 표현식 해석기로 등록 전에 한 번 확인해 보세요. 로그에 찍힌 유닉스 시각을 사람이 읽는 시간으로 바꿀 땐 타임스탬프 변환기가 편하고, 초·밀리초 단위 실수는 초와 밀리초 단위 버그 글에서 정리했어요.

#크론 표현식#cron 문법#crontab 예제#크론 매 5분#cron 요일 날짜 OR#cron 타임존 UTC KST#GitHub Actions schedule cron#쿠버네티스 CronJob
← 이전 글2진수 16진수 변환 원리, 자리값·4비트 묶음·2의 보수 총정리다음 글 →CSV 엑셀 한글 깨짐 해결, UTF-8 BOM·CP949·따옴표 규칙 정리

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →