멈춘 프로세스와 남아 있는 예약은 따로 확인해야 한다

예약 작업을 중지했는데 잠시 뒤 새 PID가 생긴다. 명령이 실패했다고 생각하기 쉽지만, 실행 중인 서비스는 멈췄고 타이머가 다음 실행을 요청했을 수도 있다. 이번 글에서는 systemd 사용자 서비스와 타이머를 따로 만들어 이 차이를 확인했다.

결과부터 말하면 서비스에 stop을 보낸 것만으로 다음 실행이 사라지지는 않았다. 타이머를 disable한 뒤에도 이미 켜져 있던 타이머는 동작했다. 타이머에 disable --now를 적용하고 서비스도 stop한 뒤에야 두 유닛이 모두 inactive가 됐다.

이 글은 기존 운영 작업을 중지한 경험담이 아니다. 고유 이름의 임시 사용자 유닛을 만든 격리 실험이다. 작업은 임시 파일에 자기 PID를 한 줄 기록한 뒤 대기하는 작은 Python 프로그램이며, 실제 데이터나 운영 서비스는 건드리지 않았다.

서비스의 자동 재시작을 끄고 타이머만 남겼다

2026-09-21, Linux·Python 3.10.12·systemd 249 환경에서 실행했다. 서비스를 Restart=no로 설정해 프로세스 종료에 따른 자체 재시작과 타이머의 다음 실행을 구분했다. 작업은 최대 30초 대기하지만 실험에서는 시작을 확인한 직후 중지했다.

[Service]
Type=simple
ExecStart=/usr/bin/python3 /실험용/worker.py /실험용/runs.txt
Restart=no

# 별도의 .timer 파일
[Timer]
OnActiveSec=2s
OnUnitInactiveSec=2s
AccuracySec=100ms
Unit=실험용.service

[Install]
WantedBy=timers.target

위 경로와 유닛 이름은 설명용 자리표시자다. 내려받을 수 있는 스크립트가 실행 때 실제 임시 경로와 이름을 만든다. 타이머는 처음 활성화한 뒤 2초, 연결된 서비스가 비활성화된 뒤 2초를 기준으로 실행을 예약했다. OnUnitInactiveSec의 기준은 작업 시작 시각이 아니라 서비스가 비활성화된 시각이다.

실험 등록에는 systemctl --user enable --runtime --now를 사용했다. --user는 현재 사용자의 서비스 관리자를 대상으로 하고, --runtime은 이번 실행 환경에만 등록을 남긴다. 아래 표의 enabled-runtime은 영구 부팅 등록을 뜻하지 않는다. 재부팅 테스트는 하지 않았다.

disabled인데 active인 타이머가 실제로 다시 실행했다

직접 관찰한 결과 · 2026-09-21
관찰 시점누적 실행서비스타이머타이머 등록
최초 타이머 시작1activeactiveenabled-runtime
서비스만 stop 후 다음 실행2activeactiveenabled-runtime
타이머만 disable 직후2activeactivedisabled
disabled 타이머가 다시 실행3activeactivedisabled
타이머 disable --now 직후3activeinactivedisabled
서비스도 stop 후 4초 관찰3inactiveinactivedisabled
완료 파일 생성 후 수동 start3inactiveinactivedisabled

표의 누적 실행은 작업이 파일에 기록한 PID 줄 수다. 최초 실행은 1이었다. 서비스만 stop한 뒤에는 새 프로세스가 시작해 2가 됐다. 다음으로 타이머만 disable했을 때 등록 상태는 disabled로 바뀌었지만 실행 상태는 active였다. 이 상태에서 서비스를 다시 stop하자 누적 실행이 3으로 늘었다.

타이머에 disable --now를 적용한 직후에도 이미 시작한 서비스는 active였다. 타이머를 멈추는 것과 그 타이머가 실행한 작업을 끝내는 것은 별도였다. 서비스까지 stop하고 4초 동안 관찰했을 때는 누적 실행이 3에 머물렀고 서비스의 MainPID도 0이었다. 4초는 이 실험의 2초 간격보다 긴 확인 구간일 뿐, 모든 환경에서 영구 중지를 입증하는 시간은 아니다.

stop·disable·mask는 무엇을 바꾸나

명령바꾸는 것이번 실험과의 관계
stop 작업.service현재 서비스 중지살아 있는 타이머가 다시 시작했다
disable 작업.timerenable 등록 해제active 타이머는 그대로 동작했다
disable --now 작업.timer등록 해제와 타이머 중지이미 실행 중인 서비스는 별도로 중지했다
mask 작업.service유닛 활성화 자체를 차단공식 문서로 확인했으며 실행 실험은 하지 않았다

systemctl 공식 문서도 disable에 실행 중지까지 포함되지는 않는다고 설명한다. --now를 함께 쓰거나 별도로 stop해야 한다. 반대로 disable된 서비스도 수동 start나 다른 유닛의 요청으로 실행될 수 있으므로, 등록 해제만으로 모든 시작 경로를 막았다고 볼 수는 없다.

Restart=always를 발견했다고 해서 systemctl stop 뒤 재등장의 원인을 곧바로 그 설정으로 판단하지는 않는다. systemd.service 문서에서는 systemctl stop에 의한 중지는 Restart 규칙의 재시작 대상에서 제외한다. 직접 프로세스를 종료한 상황과 서비스 관리자를 통해 중지한 상황을 구분하고, 타이머나 외부 관리 프로그램의 새 시작 요청도 살펴봐야 한다. 이번 실험의 Restart 값은 no였다.

mask는 수동 시작까지 막아야 하는 유닛에 사용하는 더 강한 차단이다. 그러나 이번 글에서는 실제 mask나 복구 절차를 시험하지 않았다. 특히 직접 만든 로컬 유닛 파일이 있는 환경에서는 명령 성공 여부와 파일 배치를 확인해야 하며, 유닛 정의를 지워서 억지로 적용하는 방식으로 접근하지 않는다.

실제 작업은 실행 주체를 확인한 뒤 순서대로 멈춘다

아래는 적용 예시이며 위 표의 측정 명령을 그대로 복사한 것은 아니다. 먼저 my-job을 실제 작업 이름으로 바꿔 읽기 전용 조회를 한다. 사용자 유닛이면 예시처럼 --user를 사용하고, 시스템 유닛이면 해당 시스템 서비스 관리자를 조회해야 한다. 두 관리자의 상태는 서로 다르다.

systemctl --user status my-job.service my-job.timer
systemctl --user list-timers --all
systemctl --user show my-job.service \
  -p ActiveState -p SubState -p MainPID -p Restart -p TriggeredBy
systemctl --user show my-job.timer \
  -p ActiveState -p UnitFileState -p Triggers
systemctl --user cat my-job.service my-job.timer

작업을 계속 예약할 필요가 없고 현재 실행도 끝내려는 상황이라면, 새 시작을 요청하는 타이머를 먼저 중지한 뒤 서비스를 중지한다. 타이머와 서비스 이름이 같지 않을 수도 있으므로 Triggers와 Unit 설정을 확인한 다음 적용한다.

systemctl --user disable --now my-job.timer
systemctl --user stop my-job.service

systemctl --user show my-job.timer \
  -p ActiveState -p UnitFileState
systemctl --user show my-job.service \
  -p ActiveState -p SubState -p MainPID

이 명령에서 기대하는 것은 타이머의 inactive·disabled와 서비스의 inactive 상태다. 작업이 아직 종료 중이면 deactivating일 수 있으므로 종료 결과까지 확인한다. MainPID=0만으로 별도 프로세스나 다른 관리자에 속한 작업이 없다고 단정하지 말고, 서비스 상태와 해당 서비스의 프로세스 목록도 함께 본다.

서비스 자체도 부팅 시 시작하도록 enable했다면 그 등록도 해제해야 한다. 이번 실험 서비스는 [Install]이 없어 static이었다. static은 disable이 실패했다는 뜻이 아니며 다른 유닛의 요청으로 시작할 수 있다. 이와 별개로 같은 스크립트를 실행하는 cron, 다른 타이머, 외부 관리 프로그램이 있는지는 실제 운영 환경에서 확인해야 한다.

조치 후 읽을 값의미추가로 남는 확인
timer: inactive·disabled현재 타이머 중지·등록 해제다른 타이머/cron의 시작 요청 여부
service: inactive·MainPID=0해당 유닛의 실행 상태 종료별도 관리자·분리된 작업 존재 여부
ConditionResult=no시작 조건을 만족하지 않아 건너뜀완료 파일이 실제 검증 완료 뒤 기록됐는지

완료된 일회성 작업에는 완료 조건도 남길 수 있다

중지와 완료는 다르다. 작업을 나중에 이어갈 계획이라면 중지만으로 충분할 수 있다. 반면 모든 항목 처리와 검증까지 끝난 일회성 작업은, 다시 시작 요청이 들어왔을 때 실행할 이유가 있는지를 별도로 판단할 수 있다.

[Unit]
ConditionPathExists=!/var/lib/my-job/completed

위 설정은 해당 경로가 없을 때만 시작 조건을 만족하도록 하는 예시다. 실험에서는 /var/lib가 아닌 임시 디렉터리에 완료 파일을 만들었다. 그 뒤 수동 start 명령의 종료 코드는 0이었지만 서비스는 inactive, ConditionResult는 no였고 누적 실행도 3으로 유지됐다. 명령이 오류 없이 끝났다는 사실이 곧 작업 실행을 뜻하지는 않았다.

이 조건은 작업 결과의 정확성을 검사하지 않는다. 완료 파일을 너무 일찍 만들면 해야 할 작업까지 건너뛸 수 있다. 실제 적용에서는 결과 저장과 검증을 마친 뒤 완료 표시를 기록하고, 그 경로가 재부팅·배포 후에도 유지되는지와 작업 계정에 필요한 쓰기 권한이 있는지 확인해야 한다. 이미 실행 중인 프로세스를 중지시키는 설정도 아니므로 타이머와 서비스 종료를 대신하지 않는다.

완료 파일이 사라지거나 조건 설정을 제거하면 시작이 다시 허용된다. 같은 프로그램을 systemd 밖에서 직접 실행하면 이 유닛 조건을 거치지 않는다. 따라서 다시 실행되면 안 되는 작업은 실행 경로와 완료 상태를 함께 관리해야 한다.

이번 확인으로 말할 수 있는 범위

확인한 것은 단일 사용자 서비스와 2초 간격 타이머의 동작이다. cron, 소켓 활성화, 외부 watchdog, 재부팅, Persistent=true의 달력 일정 보충 실행은 시험하지 않았다. 실제 작업의 DB 반영 도중 중지나 종료 신호 처리도 이 단순한 대기 프로그램으로 검증할 수 없다.

실험 종료 후 임시 유닛 파일과 enable 링크가 없어졌고, 기록된 작업 PID도 남아 있지 않은 것을 확인했다. 운영에서 종료를 판단할 때도 같은 방식으로 무엇을 중지했고, 무엇이 다시 시작시킬 수 있으며, 어떤 완료 조건이 남았는지를 구체적으로 확인하는 편이 좋다.

실험 환경과 원자료

관찰 시각: 2026-09-21T14:18:01.889967+00:00
os: Linux · python: 3.10.12 · systemd: systemd 249 (249.11-0ubuntu3.22) · scope: disposable --user units, runtime enablement only

고유 이름의 사용자 서비스·타이머를 만들었다. 작업은 임시 파일에 PID 한 줄을 남긴 뒤 대기했다. 서비스의 Restart=no, 타이머의 OnUnitInactiveSec=2s 조건에서 stop과 disable을 비교했다. 종료 시 실험 유닛과 실행 프로세스를 정리했다.

확인하지 않은 범위: 재부팅, cron, 외부 watchdog, Persistent=true의 누락된 달력 일정, mask는 실행 실험하지 않았다. 중지 뒤 추가 실행 여부의 관찰 창은 4초이며 영구 중지를 증명하는 부하 시험은 아니다.

Python 3과 systemd 사용자 관리자가 동작하는 Linux 로그인 세션에서 실행합니다. 관리자 권한과 네트워크는 필요하지 않습니다. --user 관리자에 고유 이름의 임시 서비스·타이머를 실제 등록하고, finally에서 중지·등록 해제·파일 삭제를 수행합니다. 기존 운영 유닛은 변경하지 않습니다. 임시 유닛이 시작한 프로세스와 임시 파일을 사용하며, 기존 결과 파일은 덮어쓰지 않습니다. 스크립트를 강제 종료해 정리 코드가 실행되지 않았다면 punchblog-timer-lab- 접두사의 남은 유닛을 확인해야 합니다.

python3 lab-systemd-timer-stop.py --output new-observations.json

참고한 공식 문서

원고 수정: 2026-09-22 · 정정 요청

이 글의 보완 기록

  • · 중지 후 상태와 완료 조건의 판정표를 추가했다.