Contents
see List정산, 알림 발송, 재고 동기화처럼 정해진 시간에 돌아야 하는 Node.js 배치 작업이 두 번 실행되면 같은 주문을 다시 처리하거나 같은 고객에게 알림을 반복 발송할 수 있습니다. 서버를 2대 이상 운영하거나 배포 직후 프로세스가 겹치는 환경에서는 cron 설정 하나만으로 실행을 보장할 수 없습니다. 해결의 핵심은 작업 시작 전에 하나의 실행 권한만 얻고, 작업별 실행 이력과 결과를 남겨 재실행 여부를 판단하는 것입니다.
중복 실행이 생기는 지점부터 구분합니다
가장 흔한 원인은 애플리케이션 인스턴스마다 스케줄러가 살아 있는 경우입니다. 예를 들어 2대의 서버가 모두 매일 01:00에 같은 정산 함수를 호출하면 작업은 2번 시작됩니다. 컨테이너 재시작, 수동 재실행, 이전 실행의 지연도 겹침을 만듭니다. 작업 이름, 대상 기준일, 실행 상태를 분리하지 않으면 운영자는 어느 실행이 정상인지 판단하기 어렵습니다.
- 작업 키는
daily-settlement:2026-09-15처럼 작업명과 기준일을 함께 사용합니다. - 실행 상태는
RUNNING,SUCCEEDED,FAILED,SKIPPED로 구분합니다. - 락에는 실제 최대 처리 시간보다 충분히 긴 만료 기준을 둡니다.
- 실패한 작업만 별도 횟수와 다음 실행 시각을 기록해 재시도합니다.
PostgreSQL 실행 이력 테이블을 먼저 만듭니다
단일 서버든 여러 서버든 완료 여부를 남길 저장소가 필요합니다. 아래 테이블은 같은 작업 키를 한 번만 생성하도록 기본 키를 사용합니다. 기준일과 작업 이름을 합친 키를 쓰면 월말 정산처럼 같은 이름의 작업도 날짜별로 분리해 추적할 수 있습니다.
CREATE TABLE batch_job_run (
job_key varchar(120) PRIMARY KEY,
status varchar(20) NOT NULL,
started_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
worker_id varchar(80) NOT NULL,
error_message text
);
CREATE INDEX batch_job_run_status_started_idx
ON batch_job_run (status, started_at);
작업 시작 시에는 INSERT ... ON CONFLICT DO NOTHING으로 실행 권한을 시도합니다. 반환 행 수가 1이면 이 프로세스가 처리하고, 0이면 다른 프로세스가 이미 처리했거나 완료한 상태입니다. 이전 실행이 비정상 종료돼 RUNNING으로 남은 경우까지 무조건 건너뛰면 작업이 영구히 멈출 수 있으므로 운영 기준 시간을 정해야 합니다.
Node.js에서 실행 권한을 확인하는 코드
아래 예시는 pg 클라이언트를 사용하는 방식입니다. 작업 기준일은 서버의 로컬 시간이 아니라 업무 기준 시간대에서 계산해야 합니다. 한국 시간 일 배치라면 애플리케이션과 데이터베이스의 시간대 해석을 확인하고, 로그에도 동일한 기준일을 남깁니다.
async function acquireJob(client, jobKey, workerId) {
const result = await client.query(
'INSERT INTO batch_job_run (job_key, status, worker_id) ' +
'VALUES ($1, $2, $3) ON CONFLICT (job_key) DO NOTHING',
[jobKey, 'RUNNING', workerId],
);
return result.rowCount === 1;
}
async function runDailySettlement(client, businessDate, workerId) {
const jobKey = 'daily-settlement:' + businessDate;
if (!(await acquireJob(client, jobKey, workerId))) return;
try {
await createSettlementRows(client, businessDate);
await client.query(
'UPDATE batch_job_run SET status = $2, finished_at = now() WHERE job_key = $1',
[jobKey, 'SUCCEEDED'],
);
} catch (error) {
await client.query(
'UPDATE batch_job_run SET status = $2, finished_at = now(), error_message = $3 WHERE job_key = $1',
[jobKey, 'FAILED', String(error).slice(0, 1000)],
);
throw error;
}
}
락 만료와 재실행은 별도 절차로 다룹니다
단순히 30분이 지났다고 RUNNING 행을 삭제하는 방식은 느린 정상 작업과 충돌할 수 있습니다. 먼저 작업별 최대 처리 시간을 정합니다. 예를 들어 평소 2분, 최장 10분인 작업이라면 20분을 점검 기준으로 둘 수 있습니다. 20분을 넘긴 실행은 처리 건수, 프로세스 로그, 데이터베이스 연결을 확인한 뒤에만 실패 상태로 바꿉니다. 상태 변경과 재실행을 같은 관리 화면 또는 운영 명령으로 제한하면 여러 사람이 동시에 다시 시작하는 문제도 줄일 수 있습니다.
SELECT job_key, status, started_at, finished_at, worker_id
FROM batch_job_run
WHERE status = 'RUNNING'
AND started_at < now() - interval '20 minutes'
ORDER BY started_at ASC;
조회 결과가 있으면 해당 작업이 실제로 살아 있는지 먼저 확인합니다. 정상 프로세스가 없다면 FAILED로 종료 사유를 남기고, 새로운 실행은 새 작업 키를 만들지 말고 승인된 재실행 흐름에서 처리합니다. 정산 데이터처럼 금액이 바뀌는 작업은 각 처리 대상에도 고유 키와 유니크 제약을 두어, 배치 락이 풀린 뒤 재시도해도 동일 레코드가 두 번 생성되지 않게 해야 합니다.
배포와 모니터링에서 확인할 항목
- 스케줄러 실행 주체를 1개 서비스로 고정하거나 모든 인스턴스에서 공유 실행 이력을 사용합니다.
- 로그에는 작업 키, 기준일, 시작·종료 시각, 처리 건수, 실패 건수를 구조화해 남깁니다.
FAILED상태가 1회라도 생기면 담당자가 원인과 재실행 여부를 판단할 수 있도록 알림을 연결합니다.- 배포 전에는 같은 기준일로 2개 프로세스를 동시에 실행해 한 쪽만 권한을 얻는지 검증합니다.
배치 작업의 안전성은 스케줄을 한 번 등록하는 데서 끝나지 않습니다. 실행 권한, 상태 기록, 만료 점검, 데이터 자체의 중복 방지를 함께 설계해야 서버 증설과 재시도 상황에서도 결과를 신뢰할 수 있습니다.
소프트모아의 서비스와 포트폴리오는 softmoa.com에서 확인할 수 있습니다.
소프트모아는 해당 시스템을 구축합니다. 문의하기
javascript
| No | 작성일 | Title |
|---|---|---|
| 3479 | 2026. 09. 15. | Node.js 배치 작업이 두 번 실행될 때: 분산 락과 실행 이력으로 중복 실행 막기 |
| 3447 | 2026. 09. 06. | Node.js 대용량 파일 업로드를 안정적으로 처리하는 방법: 스트림·용량 제한·임시 파일 관리 |
| 3416 | 2026. 08. 29. | Node.js 환경변수 설정 실수를 줄이는 방법: 시작 시 검증과 타입 변환 실전 가이드 |
| 3384 | 2026. 08. 21. | Node.js API에서 요청 취소를 제대로 처리하는 방법: AbortSignal로 외부 작업 정리하기 |
| 3352 | 2026. 08. 13. | Node.js 응답이 간헐적으로 느릴 때: 이벤트 루프 지연 측정과 CPU 작업 분리 방법 |
| 3320 | 2026. 08. 04. | JavaScript fetch 요청을 안전하게 만드는 방법: timeout·재시도·오류 처리 실전 패턴 |
| 3288 | 2026. 07. 27. | Node.js 서버 무중단 배포를 위한 Graceful Shutdown 구현 가이드 |
| 3257 | 2026. 07. 19. | Node.js 대용량 파일 처리 가이드: 스트림과 backpressure로 메모리 사용량 제어하기 |
| 3232 | 2026. 07. 11. | Node.js 실무형 에러 처리: async/await에서 실패를 일관되게 분류하고 복구 속도 높이기 |
| 3174 | 2026. 07. 02. | Node.js 환경 변수 검증 가이드: process.env를 안전한 설정 객체로 바꾸기 |