배치 작업의 실행 시간 초과와 다음 주기와의 중첩
증상 확인: 배치 작업이 예정보다 길게 실행되거나 중복 실행됨
주기적으로 실행되는 배치 작업(예: 데이터 백업, 리포트 생성, 정기적인 데이터 처리)이 예상 실행 시간을 초과하여 완료되지 않습니다. 이로 인해 동일한 작업의 다음 주기 실행이 시작되어 두 개의 동일한 프로세스가 동시에 실행되거나, 시스템 자원(CPU, 메모리, I/O)을 과도하게 점유하여 전체 서버 성능이 저하됩니다. 심각한 경우, 데이터 무결성이 훼손되거나 애플리케이션이 응답하지 않는 상태에 빠질 수 있습니다.
원인 분석: 잠금(Lock) 관리 실패와 모니터링 부재
이 문제의 근본 원인은 크게 두 가지로 구분됩니다. 첫째, 작업 실행 상태를 제어하는 메커니즘(잠금 파일, 데이터베이스 플래그, 메모리 키)의 실패입니다. 작업이 시작될 때 생성된 잠금이 비정상 종료 시 제거되지 않아 다음 주기에서 ‘작업이 이미 실행 중’이라는 판단을 하지 못합니다, 둘째, 작업 실행 시간에 대한 정확한 예측과 모니터링이 부재합니다. 데이터 양의 증가나 외부 시스템 지연으로 인해 실행 시간이 점진적으로 늘어났지만, 이에 대한 경고나 주기 조정이 이루어지지 않았습니다.
해결 방법 1: 기본적인 잠금(Lock) 메커니즘 구현 및 강화
가장 먼저, 동일한 배치 작업의 중복 실행을 방지할 수 있는 잠금 시스템을 구현하거나 기존 시스템을 점검해야 합니다. 이는 스크립트나 애플리케이션의 가장 처음에 위치해야 합니다.
- 파일 기반 잠금(File Lock) 점검: 작업 시작 시 특정 경로(예:
/var/run/batch_job_name.pid)에 잠금 파일을 생성합니다. 생성 전, 해당 파일이 이미 존재하는지 확인합니다.- 파일이 존재하고, 파일 내 기록된 프로세스 ID(PID)가 현재 시스템에서 여전히 실행 중인지 확인합니다(
ps -p [PID]). - 실행 중이지 않다면, 이전 작업이 비정상 종료된 것이므로 잠금 파일을 삭제하고 새 작업을 진행합니다.
- 실행 중이라면, 현재 작업을 즉시 종료하고 로그에 “중복 실행 방지” 메시지를 기록합니다.
- 파일이 존재하고, 파일 내 기록된 프로세스 ID(PID)가 현재 시스템에서 여전히 실행 중인지 확인합니다(
- 데이터베이스 플래그 활용: 공유 데이터베이스에 `batch_job_status` 테이블을 생성하고, 작업 시작 시 `status`를 ‘RUNNING’으로, `started_at`을 현재 시간으로 업데이트합니다. 작업 종료 시 ‘COMPLETED’로 변경합니다. 다음 주기 작업은 시작 전 ‘RUNNING’ 상태의 작업이 있는지, 그리고 `started_at` 시간이 비정상적으로 오래된(예: 예상 실행시간의 2배) 것은 아닌지 확인합니다.
- 주의사항: 잠금 파일 생성이나 DB 업데이트 작업 자체도 원자적(atomic)이어야 합니다. 동시에 두 프로세스가 “잠금이 없음”을 판단하는 것을 방지하기 위해, 파일 생성 시 `O_EXCL` 플래그를 사용하거나 데이터베이스의 `SELECT FOR UPDATE` 문과 같은 동시성 제어 기법을 사용해야 합니다.
해결 방법 2: 작업 실행 시간 모니터링 및 타임아웃 설정
잠금 메커니즘으로 중복 실행은 방지했더라도. 작업 자체가 끝없이 실행되면 시스템 자원을 계속 점유하는 문제가 남아 있습니다. 작업에 명시적인 타임아웃과 모니터링을 도입해야 합니다.

스크립트/애플리케이션 내부 타임아웃
배치 작업 코드 내에서 장시간 지연을 유발할 수 있는 구간(예: 대용량 데이터베이스 쿼리, 외부 API 호출)에 개별 타임아웃을 설정합니다.
- 데이터베이스 쿼리: SQL 쿼리에 MAX_EXECUTION_TIME 힌트를 추가하거나, 데이터베이스 연결 설정에서 쿼리 타임아웃 값을 설정합니다. 또한, 근본적으로 DB 트랜잭션의 일관성 수준(Consistency Level) 오설정으로 인해 불필요한 락(Lock) 경합이 발생하여 쿼리 시간이 길어지는 것은 아닌지 병행하여 검토해야 합니다.
- 외부 호출: HTTP 요청이나 소켓 통신 시 연결 타임아웃(connection timeout)과 읽기 타임아웃(read timeout) 값을 반드시 명시합니다. 이 값은 주기적인 실행 간격을 고려하여 설정해야 합니다.
- 전체 작업 타임아웃: 스크립트 시작 시 기준 시간을 기록하고. 주요 루프 내에서 현재 시간과 비교하여 총 실행 시간 한계(예: 예상 시간의 150%)를 초과하면 모든 자원을 정리하고 로그를 기록한 후 강제 종료합니다.
외부 모니터링 도구 연동
작업 실행을 스케줄러(Cron, Jenkins, Airflow 등)에 의존한다면, 해당 도구의 기능을 최대한 활용합니다.
- Cron 작업 감싸기: Cron에서 직접 명령어를 실행하기보다, 타임아웃과 로깅을 처리하는 래퍼 스크립트를 실행하도록 합니다. 가령, `timeout` 명령어를 사용하여 최대 실행 시간을 제한할 수 있습니다:
timeout 3600 /path/to/your/real_script.sh - 전용 스케줄러 사용: Jenkins나 Apache Airflow와 같은 도구는 작업 실행 시간 기록, 실패 알림, 이전 작업 실행 중일 때의 큐잉(queuing) 또는 스킵 정책을 내장하고 있습니다. 이러한 고급 기능을 활용하는 것이 장기적으로 더 안정적입니다.
- 모니터링 시스템 알림: 작업이 완료되면 마지막 단계에서 모니터링 시스템(예: Prometheus, Datadog)에 메트릭을 전송하거나, 성공/실패 여부를 알림 시스템(슬랙, 이메일)으로 보고하도록 합니다. 실행 시간이 점점 증가하는 추세를 그래프로 시각화하여 사전에 주기 조정이 가능하도록 합니다.
해결 방법 3: 작업 설계 재검토 및 분산 처리
위의 방법들이 임시 조치라면, 이 방법은 근본적인 해결책입니다. 실행 시간이 주기를 초과할 정도로 길어진다는 것은 작업 자체의 설계나 리소스 한계에 도달했음을 의미할 수 있습니다.
- 작업 분할(Partitioning): 단일 대용량 작업을 논리적으로 독립된 여러 개의 소규모 작업으로 분할합니다. 예를 들어, ‘전체 사용자 데이터 백업’을 ‘A-M으로 시작하는 사용자’, ‘N-Z로 시작하는 사용자’로 나누어 별도의 주기 또는 병렬로 실행합니다.
- 증분 처리(Incremental Processing) 도입: 매번 전체 데이터를 처리하는 대신, 마지막 실행 이후 변경된 데이터만 처리하는 방식으로 전환합니다. 이를 위해서는 데이터에 안정적인 ‘수정 시간戳’이 필요하며, 작업이 마지막으로 처리한 지점을 안전하게 저장해야 합니다.
- 비동기 및 큐 시스템 활용: 작업의 실행 주기를 트리거 역할만 하도록 변경합니다. 실제 무거운 처리 작업은 메시지 큐(RabbitMQ, Kafka)에 작업 항목을 발행하고, 별도의 워커(Worker) 프로세스 풀이 큐에서 작업을 가져와 처리하도록 합니다. 이렇게 하면 주기적인 트리거는 즉시 종료되고, 처리량은 워커의 수를 조정하여 유연하게 제어할 수 있습니다.
- 리소스 병목 현상 분석: 작업 실행 중 시스템 모니터링 도구(htop, iotop, nmon)를 사용하여 CPU, 메모리, 디스크 I/O, 네트워크 사용률을 확인합니다, 특정 리소스가 80% 이상 지속적으로 사용된다면 해당 부분이 병목 현상입니다. 데이터베이스 인덱스 추가, 쿼리 최적화, 더 빠른 스토리지로의 마이그레이션 등을 고려해야 합니다.
주의사항 및 예방 조치
배치 작업의 안정성은 시스템 전체의 신뢰성과 직결됩니다. 다음과 같은 예방 조치를 체계적으로 도입해야 합니다.
- 상세 로깅 필수: 작업의 시작, 각 주요 단계의 완료, 예상 소요 시간, 최종 완료 또는 실패 사유를 반드시 로그 파일에 기록합니다. 로그는 작업별, 일자별로 분리되어 관리되어야 합니다.
- 재시도(Retry) 로직의 신중한 설계: 일시적인 오류에 대한 재시도 로직은 필수적이지만, 재시도로 인해 작업 시간이 불필요하게 늘어나거나 중복 데이터가 생성되지 않도록 주의해야 합니다. 재시도는 ‘지수 백오프’ 방식으로 구현하고, 최대 재시도 횟수를 명확히 제한해야 합니다.
- 정기적인 작업 성능 검토: 분기별 또는 반기별로 주요 배치 작업의 실행 시간 추이를 분석합니다. 지속적인 증가 추세가 보이면 해결 방법 3에 명시된 근본적인 재설계를 위한 프로젝트를 수립해야 합니다.
- 재난 복구(DR) 시나리오 테스트: 배치 작업이 의존하는 데이터베이스나 외부 시스템이 장애를 일으켰을 때, 배치 작업이 어떻게 동작해야 하는지 시나리오를 정의하고 정기적으로 테스트합니다. 무한 대기 상태에 빠지지 않고, 명시적으로 실패하여 관리자에게 알림을 보내는 것이 바람직합니다.
전문가 팁: 잠금의 정리(Cleanup)는 시작보다 더 중요함
강력한 잠금 메커니즘을 구현했더라도, 작업이 성공적으로 끝났을 때 해당 잠금을 반드시 정리하는 코드를 작성하십시오. 이 코드는 `try…catch…finally` 구문의 `finally` 블록이나, 시그널 핸들러(SIGTERM, SIGINT)에 배치하여 작업이 어떤 경로로 종료되더라도 실행되도록 보장해야 합니다. 정리되지 않은 잠금은 시스템 재시작 시 초기화 스크립트를 통해 일괄 삭제하는 절차도 함께 마련하는 것이 좋습니다. 실행 시간 모니터링은 평균값이 아닌 95번째 또는 99번째 백분위수(P95. P99)를 기준으로 주기를 설정해야 예상치 못한 지연에 대한 버퍼를 확보할 수 있습니다.