Spring/Quartz Cron이 표준 crontab과 다른 필드 하나 — 초(seconds) 필드의 함정
서버 운영 경험이 있는 개발자라면 0 9 * * MON-FRI처럼 5개 필드로 된 crontab 표현식에 익숙합니다. 그런데 Spring Boot 프로젝트에서 @Scheduled(cron="0 9 * * MON-FRI")를 그대로 붙여넣으면 애플리케이션이 시작하자마자 IllegalArgumentException을 던지며 죽습니다. 표현식 자체엔 아무 문제가 없어 보이는데 왜 실패할까요. 답은 필드 개수에 있습니다 — Spring과 Quartz는 crontab에는 없는 초(seconds) 필드가 맨 앞에 하나 더 붙습니다.
1. 필드 개수부터 다르다: 5개 vs 6개
표준 Linux crontab은 분 시 일 월 요일 5개 필드로 최소 단위가 "분"입니다. 반면 Spring의 cron 파서는 초 분 시 일 월 요일 6개 필드를 정확히 기대합니다. 그래서 "매일 오전 9시 정각, 평일에만"을 Spring에서 쓰려면 맨 앞에 초 필드 0을 추가해 0 0 9 * * MON-FRI로 써야 합니다. 필드가 5개면 파서가 즉시 예외를 던지고, 실수로 6번째 필드 자리를 밀려 쓰면(예: 초 자리에 분 값을 넣는 식) 애플리케이션은 정상 기동되지만 의도한 시각과 전혀 다른 시점에 실행되는 조용한 버그가 됩니다.
| 구분 | 필드 순서 | 필드 수 | 특수문자 |
|---|---|---|---|
| 표준 crontab | 분 시 일 월 요일 | 5개 | * , - / |
| Spring @Scheduled | 초 분 시 일 월 요일 | 6개 | * , - / |
| Quartz Scheduler | 초 분 시 일 월 요일 [연도] | 6~7개 | * , - / L W # |
2. Quartz만의 제약: day-of-month와 day-of-week는 함께 못 쓴다
Quartz는 필드 개수뿐 아니라 문법 규칙도 하나 더 있습니다. 일(day-of-month)과 요일(day-of-week) 필드 중 정확히 하나는 반드시 물음표(?)로 "미지정"을 표시해야 합니다. crontab이나 Spring cron에서는 두 필드에 모두 *를 넣어도 문제없지만, Quartz는 이 조합을 모호하다고 보고 파싱 단계에서 거부합니다. "매월 15일"처럼 날짜로만 지정할 땐 요일 자리에 ?를 써서 0 0 9 15 * ?, "매주 월요일"처럼 요일로만 지정할 땐 일 자리에 ?를 써서 0 0 9 ? * MON으로 적습니다.
3. L·W·# — crontab엔 없는 Quartz/Spring 전용 문법
매월 마지막 날, 또는 매월 가장 가까운 평일을 지정하고 싶을 때 Quartz와 Spring cron은 L(Last)과 W(Weekday) 문자를 지원합니다. 0 0 L * *는 매월 마지막 날 자정, 0 0 15W * *는 매월 15일에 가장 가까운 평일을 의미합니다. 하지만 이는 POSIX 표준 crontab 명세엔 없는 확장 문법이라, 같은 표현식을 서버 crontab에 등록하면 무시되거나 문법 오류로 처리됩니다. crontab에서 "매월 말일"이 필요하면 date -d 등으로 마지막 날짜를 직접 계산하는 셸 스크립트를 만들어 등록하는 우회가 필요합니다.
4. 정리
- 필드 개수가 다르다: crontab 5개, Spring/Quartz는 맨 앞에 초가 붙어 6개(Quartz는 연도까지 최대 7개).
- crontab 표현식을 그대로 복사하면 예외 발생: Spring은 필드 부족 시 IllegalArgumentException을 던집니다.
- Quartz는 day-of-month/day-of-week 동시지정 금지: 둘 중 하나는 반드시
?. - L/W/# 문법은 Quartz·Spring 전용: 표준 crontab엔 없으므로 서버 등록 시 별도 스크립트로 우회해야 합니다.
자주 묻는 질문
Q. crontab의 "0 9 * * MON-FRI"를 Spring @Scheduled에 그대로 넣으면 어떻게 되나요?
A. IllegalArgumentException이 발생합니다. Spring의 cron 파서는 필드 개수를 정확히 6개(초 분 시 일 월 요일)로 기대하는데, 5개짜리 표준 crontab 문자열을 넣으면 필드 하나가 모자라 파싱에 실패합니다. 앞에 초 필드를 추가해 "0 0 9 * * MON-FRI"로 바꿔야 매일 오전 9시 정각(0초)에 실행됩니다.
Q. Quartz cron은 day-of-month와 day-of-week를 동시에 특정 값으로 지정할 수 있나요?
A. 없습니다. Quartz는 두 필드 중 정확히 하나는 반드시 물음표(?)로 '미지정'을 표시해야 합니다. "매월 15일에 실행"이면 요일 자리에 ?를 써서 "0 0 9 15 * ?"처럼, "매주 월요일에 실행"이면 일 자리에 ?를 써서 "0 0 9 ? * MON"처럼 적습니다. 둘 다 *를 쓰거나 둘 다 구체값을 쓰면 파서가 예외를 던집니다.
Q. 표준 Linux crontab에서는 L(마지막)이나 W(가장 가까운 평일)를 쓸 수 없나요?
A. 쓸 수 없습니다. L·W·#(N번째 요일) 같은 특수 문자는 Quartz와 Spring이 자체적으로 확장한 문법이며 POSIX 표준 crontab 명세엔 없습니다. crond에 "0 0 L * *"를 등록하면 무시되거나 오류가 나므로, 매월 말일 실행이 필요하면 실제 일수를 계산하는 별도 스크립트를 crontab에 등록하는 우회가 필요합니다.
Q. Spring과 Quartz의 초 필드 위치는 같은가요?
A. 네, 둘 다 맨 앞(첫 번째 필드)이 초입니다. 순서는 초 분 시 일 월 요일 순으로 동일하고, Quartz는 여기에 선택적으로 연도 필드를 맨 뒤에 하나 더 추가할 수 있어 최대 7개 필드까지 씁니다.
Q. */5 * * * * 같은 표현식은 Spring에서도 그대로 쓸 수 있나요?
A. 표현식 자체는 쓸 수 있지만 필드 위치가 다르게 해석됩니다. crontab에서 */5 * * * *는 "5분마다"지만, 이 문자열을 Spring에 그대로 넣으면 6개 필드 규칙상 첫 필드(초)가 */5로 해석되어 "5초마다"가 됩니다. 분 단위 반복이 필요하면 초 필드에 0을 넣고 분 필드에 */5를 써야 합니다.
Q. 서버가 꺼져 있어 Quartz 작업이 누락되면 어떻게 되나요?
A. Quartz는 misfire(놓친 실행) 정책을 설정할 수 있어, 서버 복구 시 놓친 작업을 즉시 1회 재실행하거나 무시하도록 선택할 수 있습니다. 반면 표준 crontab은 이런 재실행 개념 자체가 없어 다음 예정 시각까지 그냥 기다립니다.
Q. Quartz의 연도 필드는 언제 쓰나요?
A. 특정 연도에만 한 번 실행되는 일회성 작업에 씁니다. 예를 들어 "0 0 9 1 1 ? 2027"은 2027년 1월 1일 오전 9시에 한 번만 실행됩니다. 생략하면 매년 반복되는 것으로 취급됩니다.
Q. 이 사이트 Cron 생성기로 만든 표현식을 Spring에 바로 쓸 수 있나요?
A. MODOO HUB의 Cron 표현식 생성기는 표준 5필드 crontab 기준으로 동작합니다. Spring 프로젝트에 쓰려면 생성된 표현식 맨 앞에 초 필드(보통 0)를 직접 추가해야 합니다.