정해진 시간마다 실행하는 수집·백업 작업이 평소보다 오래 걸리면 다음 실행 시각이 먼저 올 수 있다. 실행 간격을 늘리는 것만으로 모든 겹침을 막았다고 보기는 어렵다. 작업 시작을 허용할지 판단하는 장치를 두고, 실행하지 못한 회차를 어떻게 처리할지 정해야 한다.
시간을 재는 대신 겹치는 상태를 확정했다

이번 실험은 실제 cron이나 수집기를 수정하지 않았다. Python 자식 프로세스 두 개를 만들고 첫 번째가 작업 구간에 진입한 뒤 표준 입력을 기다리게 했다. 첫 프로세스가 끝나지 않은 상태에서 두 번째를 시작했으므로, 기계 속도와 짧은 sleep 타이밍에 기대지 않고 실행 겹침을 비교할 수 있었다. 두 프로세스는 같은 임시 로컬 파일을 열었다.
잠금을 쓰지 않는 경우에는 두 작업 모두 ENTER를 출력했다. flock의 배타 잠금과 비차단 옵션을 사용한 경우에는 첫 작업만 ENTER를 출력하고 두 번째는 SKIP으로 끝났다. 이 표시는 실험을 위한 약속이다. SKIP을 실제 작업 성공으로 해석해서는 안 된다.
| 상황 | 뒤에 시작한 작업 | 의미 |
|---|---|---|
| 잠금 없이 첫 작업 실행 중 | ENTER | 두 작업 모두 진입 |
| 배타 잠금을 첫 작업이 보유 | SKIP | 두 번째는 본문 실행 안 함 |
| 첫 작업 정상 종료 후 | ENTER | 다음 작업 진입 |
| 잠금 보유 프로세스 종료 코드 17 | ENTER | 파일이 남아도 다음 작업 진입 |
파일이 있다는 것과 잠금을 잡고 있다는 것은 다르다
job.lock 파일이 보이면 작업 중이라고 간주하는 방식은 파일 자체를 상태 표시로 쓴다. 작업이 파일을 지우기 전에 종료되면 표시만 남을 수 있다. 존재 여부 확인과 생성 사이에 다른 작업이 끼어드는 문제도 따져야 한다. 이 글의 실험은 그 존재 검사 구현을 실행한 것은 아니며, 아래에서는 커널 잠금이 남은 파일과 어떻게 구분되는지 확인했다.
잠금을 가진 자식 프로세스를 os._exit(17)로 종료시켰다. 종료 후 job.lock 파일은 남아 있었지만 다음 프로세스는 잠금을 잡고 ENTER를 출력했다. 파일을 지워야 다시 시작할 수 있는 상태가 아니었다. 이번 자식은 잠금 파일 디스크립터를 다른 자식에게 넘기지 않았다. Linux 문서에 따르면 잠금과 연결된 모든 파일 디스크립터가 닫히는 조건이 중요하므로, 별도 하위 작업을 실행하는 구조에는 이 결과를 그대로 확대하면 안 된다.
작업 본문을 잠금이 살아 있는 범위 안에 둔다

import fcntl
def run_job():
# 모든 실행이 동일한 로컬 파일을 사용해야 한다.
with open('/tmp/example-job.lock', 'a') as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
print('SKIP: previous run still active')
return False
# 이 범위 안에서 실제 작업을 동기적으로 완료한다.
print('RUN')
return True
run_job()LOCK_EX는 배타 잠금, LOCK_NB는 잠금을 바로 얻지 못하면 기다리지 않는 선택이다. 예제 경로는 동작 설명용이다. 운영에서는 서비스 계정만 관리할 수 있는 고정 로컬 경로를 정하고, 스케줄러 실행과 수동 실행도 같은 파일을 사용해야 한다. 잠금을 잡지 않는 다른 프로그램의 쓰기까지 이 예제가 막아 주지는 않는다.
with 블록을 나온 뒤 별도 백그라운드 프로세스가 실제 작업을 이어가게 만들면 잠금 보유 시간과 작업 시간이 달라질 수 있다. 잠금 파일을 실행할 때마다 삭제·재생성하는 방식도 피해야 한다. 이름은 같아도 다른 파일을 열게 되면 동일한 대상에 대한 잠금이라는 전제가 깨질 수 있다.
건너뛰기와 대기는 업무에 따라 선택한다
| 작업의 목적 | 검토할 선택 | 남겨야 할 기록 |
|---|---|---|
| 최신 상태를 주기적으로 갱신 | 이전 회차 실행 중이면 이번 회차 건너뛰기 | 마지막 성공 시각과 연속 건너뛴 횟수 |
| 각 회차의 입력을 빠짐없이 처리 | 입력을 보관하는 큐와 처리 상태 설계 | 처리 완료한 입력 범위 |
| 같은 데이터 재처리 가능 | 결과 중복을 막는 별도 식별자·저장 규칙 | 재시도와 이미 반영된 결과 |
이번 스크립트는 비차단 건너뛰기만 시험했다. 대기열이나 모든 회차 실행을 구현한 것이 아니다. “중복 프로세스가 없다”와 “입력을 한 번만 반영했다”는 서로 다른 확인이다. 외부 작업이 끝난 직후 완료 표시 전에 프로세스가 죽으면 재시도 시 같은 일을 다시 할 수 있어, 잠금 외의 결과 관리가 필요하다.
| 적용 전 선택 | 실제로 정할 값 | 틀리면 생길 수 있는 일 |
|---|---|---|
| 잠금 대상 | 모든 실행이 공유하는 동일한 로컬 경로 | 서로 다른 파일에 잠금을 잡아 동시에 실행 |
| 실행 계정 | 잠금 디렉터리와 파일을 사용할 작업 계정 | 권한 오류를 중복 실행으로 오인 |
| 중복 요청 정책 | 즉시 SKIP 또는 정해진 대기 정책 | 필요한 작업 누락 또는 대기 작업 누적 |
| 작업 범위 | 실제 작업이 끝날 때까지 잠금 유지 | 백그라운드 작업만 남고 잠금이 먼저 풀림 |
- 터미널에서는 되는데 예약 작업에서는 실패한다면: 경로와 환경부터 비교하기
같은 스크립트가 다른 계정·작업 디렉터리로 실행되는지도 점검한다.
운영에서 잠금 다음으로 봐야 할 신호
모니터링은 ‘프로세스가 실행됐다’보다 마지막 정상 완료 시각을 봅니다. 가상으로 09시가 마지막 완료이고 10·11시가 모두 SKIP이면, 11시에 로그가 남아도 새 결과는 두 시간 동안 만들어지지 않은 상태입니다. 시간은 설명용 가정이고 이 실험의 실행 지연 측정이 아닙니다. 실제 기록에서는 시작·완료·실패·건너뛰기를 나누고, 완료에만 결과 버전과 시각을 연결하세요. 이번 비차단 잠금은 건너뛴 두 회차를 대기열로 복구하는 기능을 구현하지 않았습니다.
단일 서버의 로컬 파일을 공유하는 작은 작업이라면 이 방식의 전제를 이해하기 쉽다. 여러 서버에서 같은 작업을 실행하거나 네트워크 파일시스템을 사용하는 경우에는 잠금 범위와 동작을 별도로 검증해야 한다. 여기서는 한 Linux 호스트의 로컬 임시 파일만 사용했다.
편집자 의견
예약 작업의 중복 여부는 잠금 파일의 존재보다 실제 잠금 소유 범위를 확인하겠습니다. 파일이 남아 있는 상태와 다른 작업이 진입할 수 없는 상태를 혼동하면 정상 작업을 멈추거나 중복 실행을 허용할 수 있기 때문입니다.
자식 프로세스나 백그라운드 실행을 추가했다면 작업 본문이 끝날 때까지 잠금이 유지되는지 다시 검토하겠습니다. 잠금 해제를 기다리는 정책과 중복 실행을 건너뛰는 정책도 작업 목적에 맞춰 고릅니다.
판단 근거: 파일이 있다는 것과 잠금을 잡고 있다는 것은 다르다
의견 추가 2026-10-23 · 본문 자료를 바탕으로 한 편집 판단
실험 환경과 원자료
관찰 시각: 2026-09-12T15:06:44.133695+00:00
os: Linux · python: 3.10.12
임시 로컬 파일과 두 자식 프로세스. 첫 작업을 입력 대기 상태로 유지한 뒤 두 번째를 시작해 겹침을 확정했다.
확인하지 않은 범위: 실제 cron, 외부 API, 여러 호스트, NFS, 자식으로 상속된 파일 디스크립터, 작업 결과의 중복 반영은 시험하지 않았다.
Python 3이 있는 별도 Linux 실험 환경에서 실행합니다. Nginx 실험에는 Nginx도 필요합니다. 임시 파일과 스크립트가 만든 자식 프로세스만 사용하며, 기존 결과 파일은 덮어쓰지 않습니다.
python3 lab-job-lock.py --output new-observations.json참고한 공식 문서
원고 수정: 2026-10-24 · 정정 요청
이 글의 보완 기록
- · 잠금 경로·계정·중복 정책·작업 범위별 적용표를 추가했다.
- · 본문 근거에 대한 편집자의 판단과 결론이 달라지는 조건을 추가했습니다. 기존 관찰일·공식 자료 확인일은 유지했습니다.
- · 글별 적용 사례와 반례를 보강했습니다. 기존 관찰일과 원자료 확인일은 유지했습니다.