Node.js 크론 중복 실행 방지: 배치 작업을 안전하게 운영하는 실무 가드레일
💡 Key Takeaways
- 중복 실행 방지는 스케줄러 설정이 아니라 락과 멱등성 저장 구조까지 포함해야 완성됩니다.
- 실패 재시도는 횟수보다 분류 기준이 중요하며, 영구 실패는 즉시 격리해야 합니다.
- 배치 작업은 완료율보다 처리 지연과 누락 건수를 같이 추적해야 품질을 보장할 수 있습니다.
- 운영 체크리스트를 코드와 문서에 동시에 남겨야 야간 장애 대응 속도가 빨라집니다.
Node.js 크론 중복 실행 방지: 배치 작업을 안전하게 운영하는 실무 가드레일
배치 작업은 처음엔 조용히 돌아가다가, 트래픽이 커지는 시점에 문제를 드러냅니다. 저희도 정산 배치에서 비슷한 일을 겪었습니다. 로그에는 성공이 찍히는데 고객 문의는 중복 청구로 들어왔습니다. 원인을 보니 배포 직후 두 인스턴스가 같은 크론을 동시에 실행하고 있었습니다. 이런 이슈가 무서운 이유는 즉시 장애처럼 보이지 않는다는 점입니다. “잘 돈다”는 느낌과 실제 데이터 정합성이 다를 수 있습니다. 그래서 크론 운영에서는 실행 성공률보다 먼저 “같은 일을 두 번 하지 않았는가”를 기준으로 봐야 합니다.
판단 기준은 네 가지로 정리하면 실무에서 흔들리지 않습니다. 첫째, 실행 권한을 단일화해야 합니다. PM2나 Kubernetes 환경에서 같은 크론이 여러 인스턴스에서 동시에 실행되지 않도록 분산 락을 둬야 합니다. 둘째, 작업 자체를 멱등하게 만들어야 합니다. 락이 실패해도 데이터 레벨에서 중복 처리가 차단되게 해야 복구가 쉽습니다. 셋째, 실패 분류가 필요합니다. 외부 API 타임아웃처럼 재시도 가능한 실패와, 입력 데이터 오류처럼 즉시 격리해야 하는 실패를 나누지 않으면 재시도가 오히려 사고를 키웁니다. 넷째, 운영 지표를 결과 중심으로 봐야 합니다. 단순 성공 건수보다 처리 지연 시간, 누락 건수, 재시도 후 성공률을 같이 추적해야 실제 품질을 판단할 수 있습니다.
실행 절차는 복잡해 보이지만 순서만 고정하면 적용이 어렵지 않습니다. 1) 작업 시작 시 job_name + scheduled_at 조합으로 락 키를 만들고 TTL을 명시합니다. TTL은 작업 예상 시간의 2~3배로 두어 유실 락을 방지합니다. 2) 락 획득에 실패하면 즉시 종료하되, "스킵" 로그를 남겨 관측 가능하게 만듭니다. 3) 본 처리에서는 대상 레코드에 processed_at 또는 version 조건을 두어 같은 입력이 두 번 반영되지 않게 합니다. 4) 실패 시 에러 코드를 분류해 재시도 큐로 보낼지, 데드레터로 보낼지 결정합니다. 5) 마지막으로 작업 종료 이벤트를 남겨 시작-종료 쌍이 맞는지 확인합니다. 이 흐름을 기본 템플릿으로 만들면 신규 배치를 추가할 때도 위험을 크게 낮출 수 있습니다.
운영 체크리스트는 릴리스 이후 효과를 유지하는 핵심 장치입니다. 배포 전에는 크론이 활성화되는 인스턴스 수를 확인하고, 동일 작업이 중복 등록되지 않았는지 점검합니다. 배포 직후 30분 동안은 락 획득/실패 비율과 평균 실행 시간을 집중 관찰해 비정상 패턴을 빠르게 잡습니다. 주간 단위로는 작업별 지연 시간 분포를 보고, 특정 배치가 정시 기준을 반복적으로 넘기면 스케줄 간격이나 청크 크기를 조정합니다. 월간 점검에서는 "재시도 끝에 성공한 비율"과 "수동 복구가 필요한 건수"를 함께 보면서 자동화 경계를 재설정합니다. 이 지표들이 문서화돼 있으면 담당자가 바뀌어도 운영 품질이 크게 흔들리지 않습니다.
팀 단위 운영에서는 "누가 언제 어떤 기준으로 개입하는지"까지 미리 정해두는 것이 중요합니다. 예를 들어 지연 시간이 15분을 넘으면 온콜 엔지니어가 즉시 확인하고, 누락 건수가 0이 아니면 당일 내 수동 보정 여부를 결정하는 식으로 책임 경계를 선명하게 둡니다. 또한 배치 입력 데이터의 스냅샷 보관 기간을 정해두면, 장애 후 원인 분석에서 "원본이 바뀌어 재현이 안 되는" 문제를 줄일 수 있습니다. 운영 문서에는 실행 명령어뿐 아니라 비상 중지 조건, 재개 절차, 커뮤니케이션 채널까지 함께 적어두는 편이 실제 대응 속도에 더 큰 영향을 줍니다.
주의할 점도 분명합니다. 락만 믿고 멱등성을 생략하면 장애 시 수동 복구가 사실상 불가능해집니다. 반대로 멱등성만 두고 락을 생략하면 불필요한 중복 실행으로 리소스가 낭비됩니다. 재시도도 제한이 필요합니다. 외부 API 장애 구간에서 무제한 재시도는 장애를 키웁니다. 마지막으로 크론은 “한 번 작성 후 방치”되는 코드가 되기 쉬워서, 분기마다 복구 리허설을 해보는 편이 안전합니다. 배치 운영의 목표는 무장애 선언이 아니라, 문제가 났을 때 중복과 누락을 통제 가능한 범위에 두는 것입니다.