상태코드CODE

크론(cron) 표현식 완벽 정리 - 0 0 * * * 의미부터 초 단위 실행, 실시간 큐 전환까지

크론(cron) 표현식 완벽 정리 - 0 0 * * * 의미부터 초 단위 실행, 실시간 큐 전환까지

 

크론(cron) 표현식은 리눅스 크론탭부터 스프링 @Scheduled, node-cron까지 거의 모든 백엔드 환경에서 쓰이지만, 막상 0 0 * * * 같은 표현을 볼 때마다 또 검색창을 열게 됩니다. 이 글에서는 다섯 칸의 의미, 프레임워크가 해주는 크론이 유독 헷갈리는 이유, 초 단위 실행 노하우와 실시간이 필요할 때 큐로 넘어가야 하는 이유까지 한 번에 정리해 보겠습니다.

 

 

"0 0 * * * 이게 매일 자정 맞나? 매시간이었나…"
"스프링에 그대로 붙였더니 6 fields 어쩌고 에러가 뜨는데요?"
"크론으로 1초마다 돌리는 거, 그거 안 되죠?"
"분명히 어제 등록했는데 오늘 아침에 보니까 안 돌았어요."

 

 

저는 2012년에 일을 시작해 한동안은 크론을 그냥 "새벽에 배치 돌리는 거" 정도로만 알고 썼습니다. 그러다 2016년 무렵부터 트러블슈팅 쪽 일이 많아지면서, 크론 때문에 새벽에 깨거나 "분명히 등록했는데 왜 안 도냐"고 한참을 헤맨 적이 한두 번이 아니었어요. 그 과정에서 알게 된 것들을 정리해 둡니다. 외우기보다 구조를 이해해 두면, 다음부터는 검색 없이 읽을 수 있게 됩니다.

리눅스 서버 운영자, 스프링·Node 백엔드 개발자, 그리고 "왜 내 크론은 안 도는가"로 한 번이라도 고생해 본 분이라면 도움이 될 내용입니다.

📋 목차

  1. 크론이 뭐고, 왜 매번 검색하게 되는가
  2. 다섯 칸의 정체 - 분·시·일·월·요일
  3. 특수문자 네 개만 알면 끝 - * , - /
  4. 외워두면 편한 자주 쓰는 패턴
  5. 헷갈리는 1순위 - 프레임워크 크론은 칸이 다르다
  6. 크론은 안에서 어떻게 도는가 - 동작 메커니즘
  7. 초 단위는 안 된다? - sleep 노하우
  8. 실시간이 필요하면 크론을 버려라 - 큐 개념
  9. 현장에서 가장 많이 당하는 함정

1. 크론이 뭐고, 왜 매번 검색하게 되는가

크론은 유닉스 계열 시스템에서 "정해진 시각에 명령을 자동 실행"해 주는 스케줄러입니다. 데이터 백업, 로그 정리, 통계 집계 배치, 메일 발송 같은 반복 작업을 사람 손 안 거치고 돌리는 데 씁니다. 그 일정을 적는 약속된 문법이 바로 크론 표현식이고요.

문제는 이게 숫자와 별표만으로 시간을 표현하다 보니, 익숙해지기 전까지는 도무지 직관적이지 않다는 점입니다. 0 0 * * *를 보고 "매일 자정"이 바로 안 떠오르는 건 머리가 나빠서가 아니라, 그냥 사람이 읽으라고 만든 포맷이 아니기 때문이에요. 저도 한참을 그랬습니다.

2. 다섯 칸의 정체 - 분·시·일·월·요일

표준 리눅스 크론탭의 표현식은 공백으로 구분된 다섯 칸입니다. 왼쪽부터 분, 시, 일, 월, 요일 순서고요. 이 순서만 외워두면 절반은 끝납니다.

반응형
의미 범위 예시
1번째 0 ~ 59 30 → 매시 30분
2번째 0 ~ 23 9 → 오전 9시
3번째 1 ~ 31 1 → 매월 1일
4번째 1 ~ 12 6 → 6월
5번째 요일 0 ~ 7 (0·7=일요일) 1 → 월요일

요일 칸이 0부터 7까지인 게 좀 묘한데, 일요일이 0이기도 하고 7이기도 합니다. SUN, MON 같은 영문 약자로도 쓸 수 있어서, 헷갈리면 숫자 대신 이름으로 적는 편이 실수가 적습니다. 저는 협업할 때는 거의 영문 약자로 적습니다. 나중에 다른 사람이 봐도 바로 읽히거든요.

 

💡 읽는 요령
표현식은 항상 "왼쪽이 작은 단위"입니다. 분 → 시 → 일 → 월 → 요일로 갈수록 큰 단위가 됩니다.

0 0 * * *를 왼쪽부터 읽으면 "0분 / 0시 / 매일 / 매월 / 매요일" = 매일 0시 0분, 즉 자정이 됩니다.

 

3. 특수문자 네 개만 알면 끝 - * , - /

칸 순서를 알았으면, 이제 그 안에 들어가는 기호 네 개만 익히면 됩니다. 이게 크론의 거의 전부예요.

  • * (별표) - 해당 칸의 모든 값. 분 칸의 *는 "매분"이라는 뜻입니다.
  • , (쉼표) - 특정 값 여러 개. 시 칸에 9,12,18이면 9시·12시·18시.
  • - (하이픈) - 범위. 요일 칸에 1-5면 월요일부터 금요일까지(평일).
  • / (슬래시) - 간격. 분 칸에 */5면 5분마다.

딱 이 네 개입니다. */10 9-18 * * 1-5 같은 게 처음엔 암호처럼 보이지만, 조각조각 끊어 읽으면 "평일, 9시부터 18시 사이, 10분마다"로 풀립니다. 복잡해 보이는 표현식도 결국 이 네 기호의 조합일 뿐이에요.

4. 외워두면 편한 자주 쓰는 패턴

실무에서 반복적으로 쓰는 패턴은 사실 몇 개 안 됩니다. 아래 표만 즐겨찾기 해두면 검색할 일이 확 줄어듭니다.

표현식 의미
0 0 * * * 매일 자정 0시
0 9 * * * 매일 오전 9시
*/5 * * * * 5분마다
0 * * * * 매시 정각
0 0 * * 1 매주 월요일 자정
0 0 1 * * 매월 1일 자정
30 2 * * 1-5 평일 새벽 2시 30분
0 0 1 1 * 매년 1월 1일 자정

새벽 2~4시 사이에 배치를 몰아 돌리는 건 거의 관습 같은 건데, 트래픽이 가장 적은 시간대라 서버 부담이 적기 때문입니다. 다만 그 시간에 장애가 나면 깨우는 사람이 저였다는 게 함정이었죠. 새벽 배치는 실패 알림을 꼭 붙여두시길 권합니다.

5. 헷갈리는 1순위 - 프레임워크 크론은 칸이 다르다

여기서부터가 진짜 사람 잡는 부분입니다. 리눅스 크론탭은 다섯 칸인데, 스프링이나 일부 라이브러리에서 쓰는 크론은 칸이 하나 더 많습니다. 맨 앞에 "초" 칸이 붙어서 여섯 칸이 되거든요.

스프링 @Scheduled는 5.3 버전 이후로(현재 스프링 6.x도 동일하게) 여섯 칸을 요구합니다. 그래서 인터넷에서 찾은 다섯 칸짜리 표현식을 그대로 붙이면 Cron expression must consist of 6 fields 에러가 나면서 앱이 안 뜹니다. 이거 모르고 한 번씩 다 당합니다. 저도 당했고요.

항목 리눅스 crontab 스프링 @Scheduled node-cron
칸 수 5칸 6칸 (초 필수) 5 또는 6칸 (초 선택)
맨 앞 칸 초 (생략 시 분부터)
최소 단위 1분 1초 1초
실행 위치 OS 데몬(crond) 앱 프로세스 안 앱 프로세스 안
흔한 사고 PATH·권한 문제 5칸 넣으면 에러 프로세스 죽으면 잡 소실

자바 진영의 Quartz 스케줄러는 한술 더 떠서 여섯 칸 또는 일곱 칸(맨 뒤에 연도 칸이 선택적으로 붙음)을 씁니다. 그래서 "크론 표현식"이라고 다 같은 게 아니라, 내가 지금 쓰는 게 OS 크론인지, 스프링인지, Quartz인지부터 확인하는 게 첫 단추입니다.

 

 

📌 다섯 칸을 여섯 칸으로 옮길 때


리눅스용 0 9 * * *(매일 9시)를 스프링으로 옮기려면

맨 앞에 초 칸을 붙여 0 0 9 * * *로 바꾸면 됩니다.

그리고 스프링/Quartz에서는 일과 요일을 동시에 *로 두면 모호하다고 보고,

한쪽을 ?(지정 안 함)로 적어야 하는 경우가 있습니다.

에러가 나면 이 부분을 먼저 의심해 보세요.

 

6. 크론은 안에서 어떻게 도는가 - 동작 메커니즘

칸 수가 왜 다른지는 동작 방식을 보면 자연스럽게 이해됩니다. 둘은 아예 다른 물건이거든요.

리눅스의 크론은 crond라는 데몬(백그라운드 프로세스)이 담당합니다. 이 친구가 대략 1분에 한 번씩 깨어나서 크론탭 파일을 훑어보고, "지금 이 시각에 돌릴 작업 있나?" 확인한 다음 해당하는 명령을 실행합니다. 깨어나는 주기 자체가 1분이라, 구조적으로 1분보다 잘게 쪼갤 수가 없는 겁니다. 초 칸이 없는 이유가 여기에 있어요.

반면 스프링이나 node-cron의 크론은 OS가 아니라 여러분의 애플리케이션 프로세스 안에서 돕니다. 앱이 뜰 때 스케줄러용 스레드가 하나 떠서, 등록된 표현식마다 "다음 실행 시각"을 계산해 두고 그때까지 대기하다가 트리거하는 방식이에요. OS 데몬의 1분 틱에 묶여 있지 않으니 초 단위까지 잘게 제어할 수 있는 거죠.

이 차이가 실무에선 꽤 중요합니다. OS 크론은 앱이 죽어 있어도 서버만 살아 있으면 돕니다. 반대로 프레임워크 크론은 그 앱 프로세스가 살아 있어야만 돌고, 앱을 재배포하거나 프로세스가 죽으면 메모리에만 있던 스케줄도 같이 날아갑니다. node-cron이 특히 그래서, 놓치면 안 되는 중요한 잡을 인메모리 스케줄러에만 맡기는 건 위험합니다.

7. 초 단위는 안 된다? - sleep 노하우

그럼 리눅스 크론탭으로는 1초 단위 작업을 영영 못 하느냐. 정공법으로는 못 합니다. 최소 단위가 1분이니까요. 그런데 현장에서 종종 쓰는 우회법이 있습니다. 스크립트 안에 sleep을 끼워 넣는 방식이에요.

예를 들어 30초마다 돌리고 싶다면, 1분짜리 크론을 두 줄 등록하되 한 줄은 먼저 30초 자고 시작하게 만드는 겁니다.

* * * * * /home/user/job.sh
* * * * * sleep 30; /home/user/job.sh

 

위쪽 줄은 매분 0초에, 아래쪽 줄은 30초 잔 뒤 실행되니 결과적으로 30초 간격이 됩니다. 더 잘게 쪼개고 싶으면 스크립트 안에서 루프를 돌리며 sleep 간격을 조절하는 방법도 있고요.

저도 예전에 "초 단위로 좀 돌려달라"는 요청을 받고 이 방식으로 막은 적이 있습니다. 당장은 굴러갔어요. 그런데 솔직히 말하면, 이건 임시방편에 가깝습니다. 작업이 30초 안에 안 끝나면 다음 실행과 겹치고, 모니터링도 꼬이고, 무엇보다 "초 단위가 정말 필요한 상황"이라는 신호일 때가 많거든요. 그땐 그게 최선인 줄 알았는데, 지금 와서 보면 애초에 설계를 다시 봤어야 했습니다.

 

 

📌 sleep 트릭은 "급할 때 잠깐"


되긴 됩니다. 하지만 실행이 길어지면 겹치고, 실패 추적이 어려워집니다.

정말로 짧은 주기가 상시 필요하다면 프레임워크 크론(초 지원)으로 옮기거나,

아래의 큐 방식을 검토하는 편이 낫습니다.

 

8. 실시간이 필요하면 크론을 버려라 - 큐 개념

여기서 한 단계 더 나갑니다. "1초마다"도 아니고 "이벤트가 일어나는 즉시" 처리해야 한다면, 크론은 애초에 맞는 도구가 아닙니다. 크론은 본질이 폴링(polling)이에요. 시계를 보면서 "지금 할 시간인가?"를 주기적으로 확인하는 방식이니까요.

1초마다 폴링을 돌리면 어떻게 될까요. 대부분의 순회는 "할 일 없음"으로 헛돌고, 그래도 평균 0.5초쯤의 지연은 깔리고, 작업량이 없을 때도 자원은 계속 먹습니다. 저도 한때 "실시간처럼 보이게" 1초 폴링을 박아놓고 흐뭇해한 적이 있는데, 트래픽이 늘자 그 폴링이 DB를 야금야금 갉아먹고 있더군요.

이럴 때 가야 할 방향이 큐(queue)입니다. 누군가 이벤트를 발생시키면 그걸 큐에 넣고(push), 대기하던 워커가 들어온 즉시 꺼내 처리하는 구조예요. 시간을 보고 깨우는 게 아니라, 일이 생겼을 때 깨우는 방식이라 지연도 거의 없고 빈 순회도 없습니다.

구분 크론 (폴링) 큐 (이벤트)
동작 시간 보고 주기적 실행 이벤트 들어오면 즉시 처리
지연 주기만큼 발생 거의 없음
적합한 일 정기 배치·집계·정리 실시간 알림·주문 처리
대표 도구 crond, @Scheduled RabbitMQ, Kafka, Redis, SQS

물론 크론이 나쁜 게 아닙니다. "매일 새벽 정산", "매시 통계 집계"처럼 시간이 기준인 일은 크론이 정답이에요. 다만 "최대한 빨리"가 요구사항이라면 그건 시간의 문제가 아니라 이벤트의 문제고, 그때는 큐로 가는 게 맞습니다. 도구를 바꿔야 할 신호를 sleep 더 잘게 박는 걸로 버티려 하면, 나중에 더 크게 고생합니다. 제가 그랬거든요.

9. 현장에서 가장 많이 당하는 함정

마지막으로, 표현식은 멀쩡한데 크론이 안 도는 경우입니다. 배포·백업·로그 정리를 크론으로 돌릴 때 가장 많이 밟는 지뢰라 따로 떼어 둡니다.

 

 

📌 주의 - 크론탭(cron)은 유저별로 따로입니다


크론탭은 시스템에 하나만 있는 게 아니라 계정마다 따로 존재합니다.

내 계정에서

crontab -e로 등록한 작업과

sudo crontab -e(root)로 등록한 작업은 완전히 별개고,

서로의 목록에 보이지도 않습니다.

 

그래서 "어제 분명히 등록했는데 왜 안 도는지" 헤매다 보면,

알고 보니 다른 계정 크론탭에 들어가 있는 경우가 많습니다.

이게 권한과 직접 엮입니다.

cron 작업은 그 크론탭을 소유한 계정의 권한으로 실행되기 때문에,

손으로 돌릴 땐 잘 되던 스크립트가 cron에선 권한 부족으로 실패하곤 합니다.

예를 들어 내 계정 크론탭에 등록한 백업 스크립트가 www-data 소유의 로그를 읽어야 하는데

그 파일이 640이면, cron이 접근을 못 해서 조용히 실패합니다.

crontab -l로 어느 계정에 걸려 있는지,

그 계정이 대상 파일을 읽고 쓸 권한이 있는지를 꼭 같이 확인하세요.

(시스템 전체용인 /etc/crontab이나 /etc/cron.d/

실행할 계정을 적는 칸이 따로 있다는 점도 기억해두면 좋습니다.)

 

 

여기에 하나 더 보태면, 크론은 여러분이 평소 쓰는 셸 환경(.bashrc 등)을 그대로 물려받지 않습니다. PATH가 최소한으로만 잡혀 있어서, 터미널에선 잘 되던 python이나 node 명령이 크론에선 "command not found"로 죽는 일이 흔합니다. 스크립트 안에서 실행 파일을 절대경로로 적거나, 스크립트 첫머리에 필요한 환경변수를 직접 export 해두는 습관을 들이면 이 부류의 사고는 거의 막힙니다.

자주 묻는 질문

크론으로 매일 자정에 실행하려면 어떻게 쓰나요?

리눅스 크론탭에서는 0 0 * * *로 쓰면 됩니다. 왼쪽부터 "0분 / 0시 / 매일 / 매월 / 매요일"이라 매일 0시 0분, 즉 자정에 실행됩니다. 스프링 @Scheduled처럼 초 칸이 있는 환경이라면 맨 앞에 초를 붙여 0 0 0 * * *로 적어야 합니다.

스프링에서 'Cron expression must consist of 6 fields' 에러는 왜 나나요?

스프링 @Scheduled는 5.3 버전 이후로 초가 맨 앞에 붙는 여섯 칸 형식을 요구하기 때문입니다. 인터넷에서 찾은 다섯 칸짜리 표현식을 그대로 넣으면 이 에러가 납니다. 맨 앞에 초 칸을 추가해 다섯 칸을 여섯 칸으로 바꾸면 해결됩니다. 일과 요일을 둘 다 *로 두면 모호하다고 보는 경우가 있어, 한쪽을 ?로 바꿔야 할 때도 있습니다.

크론으로 초 단위 실행이 가능한가요?

리눅스 크론탭은 최소 단위가 1분이라 정공법으로는 안 됩니다. 다만 1분짜리 크론을 등록하면서 스크립트 안에 sleep을 끼워 넣으면 30초 같은 간격을 흉내 낼 수 있습니다. 스프링 @Scheduled나 node-cron처럼 초 칸을 지원하는 프레임워크 크론은 처음부터 1초 단위로 실행할 수 있습니다.

크론과 큐(메시지 큐)는 뭐가 다른가요?

크론은 시간을 기준으로 주기적으로 확인해 실행하는 폴링 방식이고, 큐는 이벤트가 들어온 즉시 처리하는 방식입니다. 정기 배치나 집계처럼 시간이 기준인 작업은 크론이 맞고, "최대한 빨리" 처리해야 하는 실시간 작업은 RabbitMQ·Kafka·Redis 같은 큐가 맞습니다.

크론탭에 등록했는데 작업이 안 도는 이유는 뭔가요?

가장 흔한 원인은 두 가지입니다. 하나는 다른 계정 크론탭에 등록한 경우로, crontab -esudo crontab -e는 별개라 서로 보이지 않습니다. 다른 하나는 PATH 문제로, 크론은 평소 셸 환경을 물려받지 않아 터미널에선 되던 명령이 'command not found'로 죽기도 합니다. 실행 파일은 절대경로로 적는 편이 안전합니다.

마무리

정리하면, 크론 표현식의 핵심은 "왼쪽부터 분·시·일·월·요일, 기호는 네 개"라는 구조 하나입니다. 거기에 프레임워크 크론은 초 칸이 하나 더 붙어 여섯 칸이 된다는 것, 그리고 그게 OS 데몬이 아니라 앱 프로세스 안에서 돈다는 차이만 얹으면 대부분의 혼란은 풀립니다.

초 단위가 급하면 sleep으로 잠깐 버틸 수는 있지만 어디까지나 임시방편이고, "즉시"가 요구사항이라면 그건 시간이 아니라 이벤트의 영역이라 큐로 넘어가는 게 맞습니다. 도구가 안 맞는다는 신호를 무시하고 크론을 억지로 잘게 쪼개다 고생한 게 저였으니, 같은 길을 굳이 다시 걷지 않으셨으면 합니다.

📚 함께 보면 좋은 글

 

리눅스 계보도 정복하기: 유닉스에서 시작된 오픈소스 언어의 거대한 역사

우리가 흔히 '리눅스'라고 부르는 운영체제는 사실 단일한 존재가 아닙니다. 🚀 마치 언어에 여러 방언이 있듯이, 리눅스는 수백 가지의 '배포판'이라는 형태로 존재하며 각기 다른 특징과 목적

devwar.tistory.com

 

APM(애플리케이션 성능 관리/모니터링) 핵심 정리: 툭하면 터지던 서버를 살려낸 개발 철학의 역

현대 소프트웨어 개발 환경에서 APM(애플리케이션 성능 관리)은 단순한 선택을 넘어 기업의 생존을 결정짓는 핵심 도구로 자리 잡았습니다.과거에는 서버가 다운되거나 서비스가 지연되면 원인

devwar.tistory.com

 

질문이 엉성하면 답도 없다 - 스택오버플로우가 바꾼 개발자의 사고방식

"이거 왜 안 되죠?" "에러 떴는데 어떻게 고쳐요?" "급해요, 알려주세요." 개발 좀 해본 분이라면 한 번쯤 이런 질문을 던졌다가 싸늘한 반응을 받아본 적 있을 겁니다. 저도 그랬어요. 그런데 그

devwar.tistory.com

 

Apache Nginx Tomcat 차이 완벽 정리 - 웹서버 30년 변천사로 이해하기

Apache, Nginx, Apache Tomcat의 차이를 한 번에 정리합니다. 셋 다 "서버"라는 이름이 붙지만 Apache와 Nginx는 웹서버, Tomcat은 WAS(서블릿 컨테이너)로 역할이 다릅니다. 이 글은 1995년부터 2026년까지 웹서버

devwar.tistory.com

 

개발 실력 늘리는 법은 함수 암기가 아니었습니다 - 제가 개발 블로그를 시작한 이유

"그 함수들 다 외우면 실력 느는 거죠?""굳이 옛날 얘기까지 알아야 하나요. 쓸 줄만 알면 되잖아요.""새로 나온 기술, 그냥 쓸 줄만 알면 되는 거 아니에요?" 저도 신입 때는 똑같이 생각했습니다.

devwar.tistory.com

반응형