크론 표현식, LLM, 그리고 당신이 저지른 범죄 조사
출처: Dev.to
대부분의 개발자들이 대충 살펴보는 것 중 하나가 바로 정규식(Regex)이다. 이에 관한 밈도 여러 개 있지만, 패턴 매칭이 중요할 때는 부정할 수 없을 정도로 유용하다. 두 번째로, 내 생각에, 크론 표현식(cron expression)이다. 정규식보다 덜 무섭고, 조금 더 예측 가능하지만 여전히 대충 살펴보는 편이다.
우연히도, 크론 표현식도 패턴 매칭에 유용하다—시간이라는 맥락에서 말이다. 반복되는 시간, 말하자면.
내가 한동안 작업해 온 프로젝트는 백그라운드 워커/잡에 크게 의존한다. 여러 흐름을 실행해 전체 시스템에 중요한 기여를 한다. 실제로 코드베이스에 있는 라우트 그룹(사용자 라우트, 출금 라우트 등)보다 잡이 다섯 배는 많을 것이다. 여기서 어디로 가는지 이미 눈치챘을 것이다. 백그라운드 잡은 동작하기 위해 크론 표현식이 필요하기 때문이다. 언제 실행할지, 얼마나 자주 실행할지를 알아야 한다. 즉, 패턴이 필요하다.
그런 잡 중 하나를 우리는 대충 mandate-debit-readiness 잡이라고 부른다. 이 잡의 책임은 출금이 가능한 위임(mandate)을 찾아 상태를 업데이트하는 것이다. 원래는 5분마다 실행되도록 설계됐으며, 거의 3개월 동안 그렇게 동작했다.
그렇다고 생각했다.
지난 주에 우리는 이 잡이 주말에 실행되지 않는 것을 발견했다. 준비된 위임이 월요일 아침 첫 번째 분까지 업데이트되지 않았다. 당연히 우리는 조사를 시작했다.
먼저 확인한 것은 워커 로직 자체였다. 주말을 건너뛰는 조건이 어딘가에 숨겨져 있지는 않을까? 그런 건 없었다. 처음엔 크론 표현식을 확인하려는 생각조차 없었고, 몇 분 뒤에야 누군가가 결국 확인했다.
그때 우리는 다음을 발견했다:
0 * * * * 1-5
Enter fullscreen mode
Exit fullscreen mode
크론 표현식을 대충 살펴보는 사람이라도 바로 눈에 띈 것이 마지막 1-5였다. 여기에 뭔가 잘못된 느낌이 들었고, 머릿속에 바로 다음과 같이 번역되었다:
월요일부터 금요일까지.
그 옆에 주석도 있었다—대략 다음과 같은 내용:
every hour on weekdays
Enter fullscreen mode
Exit fullscreen mode
첫눈에 보기에 우리는 범인을 찾은 듯했다. 잡이 주말에 실행되지 않는 이유는 우리가 명시적으로 그렇게 지정했기 때문이다. 이 표현식에 “5분마다”라는 의미가 전혀 없다는 점을 놓쳤고, 사건은 종결된 듯했다.
그렇다고 생각했다.
Gemini로 간단히 확인해 본 뒤, 우리는 1-5 제한을 제거하고 주석을 수정한 뒤 변경 사항을 배포했다.
문제 해결.
하지만 그렇지 않았다.
하루 이틀 뒤, 준비된 위임이 업데이트되는 데 너무 오래 걸린다는 보고가 들어왔다. 뭐라고? 우리는 방금 이걸 고쳤다. 주말 문제는 사라졌고, 평일에 문제가 생길 리가 없었다.
당연히 PM이 슬랙 회의를 소집했고, 우리는 다시 로그 속으로 뛰어들었다.
잡 디버깅은 매력적인 경험이다. 마치 자신이 저지른 범죄를 조사하는 탐정이 된 기분이다.
결국 로그에서 패턴이 드러났다. 잡이 기대했던 대로 몇 분마다 실행되는 것이 아니라, 매시간 실행되고 있었다. 멋지다.
코드로 돌아가 보니, 바로 그 주석이 있었다. 우리가 고친 뒤에도 무시했던 그 주석.
every hour...
Enter fullscreen mode
Exit fullscreen mode
흥미롭다.
업데이트된 크론 표현식을 Gemini에 다시 붙여넣었더니, Gemini는 “매시간”이라고 해석했다. 이상했다. 같은 표현식을 ChatGPT에 붙여넣었더니, ChatGPT는 “매분”이라고 말했다. 상황이 점점 흥미로워졌다.
같은 표현식.
다른 해석.
Claude도 시도했다. Claude는 Gemini와 의견을 같이했다. Crontab Guru도 시도했는데, 역시 Gemini와 같은 해석을 내놓았다.
이 시점에서 나는 버그 자체보다, 여러 시스템이 표준화된 것처럼 보이는 크론 표현식에 대해 서로 다른 의견을 보인다는 사실에 더 매료되었다. 그리고 만약 이 표현식이 정말 “매시간”을 의미한다면, 또 다른 질문이 남는다:
몇 달 동안 이 잡은 어떻게 정상적으로 동작했을까?
그 미스터리가 실제 버그보다 더 흥미로워졌다.
그때 깨달았다.
문제는 반드시 LLM이 아니다. 내 가정, 즉 크론 표현식은 모두가 동일하게 해석한다는 전제가 문제였다.
우리 코드베이스에는 여러 형태의 크론 포맷이 혼재한다. 어떤 잡은 다섯 필드를 사용한다:
*/5 * * * *
Enter fullscreen mode
Exit fullscreen mode
다른 잡은 여섯 필드를 사용한다:
0 0 2 * * *
Enter fullscreen mode
Exit fullscreen mode
0 0 3,22 * * *
Enter fullscreen mode
Exit fullscreen mode
그리고 여러 크론 포맷을 대화에 도입하면 상황은 의외로 복잡해진다.
일부 크론 구현은 다음과 같은 순서를 따른다:
minute hour day-of-month month day-of-week
Enter fullscreen mode
Exit fullscreen mode
다른 구현은 초(second)를 포함한다:
second minute hour day-of-month month day-of-week
Enter fullscreen mode
Exit fullscreen mode
갑자기 명백해 보였던 표현식이 그렇게 명백하지 않게 된다. 진짜 진실을 말해주는 존재는 Gemini가 아니다. ChatGPT도 아니다. Claude도 아니다. Crontab Guru도 아니다.
실제 프로덕션에서 표현식을 파싱하고 실행하는 스케줄러가 진짜 기준이다.
결국 우리는 표현식을 다음과 같이 바꾸었다:
*/5 * * * *
Enter fullscreen mode
Exit fullscreen mode
이는 명시적으로 “5분마다 실행”을 의미한다.
동작이 우리의 기대와 일치했고, 사건은 여기서 마무리되었다.
대부분은.
내게 남은 건 크론 버그 자체가 아니라, 증거에 맞춰 이론에 자신감을 너무 빨리 갖게 된 과정이었다. 표현식이 의심스러워 보였고, 주석이 그 의심을 확인시켜 주는 듯했으며, Gemini가 우리와 의견을 맞췄다. 우리는 변경을 배포하고 넘어갔지만, 결국 완전히 다른 문제를 만들었다는 사실을 알게 되었다.
돌이켜 보면, 이야기가 가장 흥미로운 부분은 서로 다른 LLM이 서로 다른 해석을 내놓았다는 것이 아니라, 그럴 합리적인 이유가 있었다는 점이다. 우리 생태계에 다섯 필드와 여섯 필드 크론 포맷이 혼합돼 있다는 사실을 깨달았을 때, 의견 차이는 더 이상 신비롭지 않았다.
진짜 교훈은 크론 표현식 자체가 아니라 가정에 관한 것이었다.
정규식과 크론 표현식은 많은 개발자들의 머릿속에서 비슷한 위치를 차지한다. 우리는 모든 세부 사항을 기억할 만큼 자주 사용하지는 않지만, 충분히 자주 사용하기 때문에 자신감이 있다. 대부분의 경우 그게 괜찮다. 그런데 어느 날, 그것들이 문제가 된다.
그리고 문제가 될 때, 우리는 스스로 저지른 범죄를 조사하고 있다는 느낌을 받는다.