예약 작업 실패 알림, 원인이 두 가지입니다 — 돌았는데 실패 vs 아예 안 돎
예약 작업이 실패했다는 알림을 여러 번 받았습니다. 문구가 다 같아서 같은 문제인 줄 알았습니다.
로그를 종류별로 세어 보니 원인이 둘로 갈렸습니다.
- A. 작업이 돌았고, 그 안에서 실패했다
- B. 작업이 아예 안 돌았다
고치는 곳이 완전히 다릅니다. 이걸 안 나누면 A를 고치는 동안 B가 계속 쌓입니다. 저는 한동안 그러고 있었습니다.
크론·예약 작업 실패를 가르는 순서
1. 알림을 받으면 실행 기록부터 봅니다 2. 돌았으면 → 작업 로그에서 응답 코드를 찾습니다 3. 안 돌았으면 → 스케줄러 설정을 봅니다 4. 알림 문구에 어느 쪽인지 넣어 다음부터 안 헤매게 합니다
왜 구분이 안 되나
증상 — 알림 문구가 같습니다. "예약 작업 실패" 한 줄로는 어느 쪽인지 모릅니다.

원인 — 알림을 결과만 보고 만들었기 때문입니다. "결과물이 없다 → 실패"로 판정하면, 돌다 실패한 것과 시작도 안 한 것이 같은 문구가 됩니다.
구분하는 법 — 실행 기록부터
해결 — 코드나 로그를 뒤지기 전에 "돌긴 돌았나" 를 먼저 봅니다.

systemctl --user list-timers "작업이름*" --no-pager
| 보이는 것 | 판정 |
|---|---|
| 마지막 실행이 예정 주기 안에 있다 | A — 돌았고 안에서 실패 |
| 마지막 실행이 한참 전이다 | B — 안 돌았다 |
| 아예 목록에 없다 | B — 등록이 풀렸다 |
이 한 줄이 조사 방향을 정합니다. A와 B는 볼 곳이 겹치지 않습니다.
경우 A — 예약 작업이 돌았는데 안에서 실패
작업 로그를 봅니다. 대개 원인이 로그에 있습니다.
| 흔한 원인 | 단서 |
|---|---|
| 인증 만료 | 401 |
| 권한 없음 | 403 |
| 대상이 없음 | 조회 결과 0건 |
| 외부 서비스 장애 | 타임아웃·5xx |
| 코드 오류 | 예외 추적 기록 |
여기서 중요한 게 하나 있습니다. 응답 코드를 로그에 남겨야 합니다. "실패했습니다" 만 적혀 있으면 위 표를 못 씁니다. 저는 이걸 나중에 추가했는데, 그 뒤로 원인 찾는 시간이 확 줄었습니다.
처음엔 로그를 짧게 두는 게 깔끔하다고 생각했습니다. 그런데 짧은 로그는 나중에 아무 도움이 안 됩니다. 저는 지금 실패할 때만 자세히 남기는 쪽으로 두고 있습니다. 성공은 한 줄, 실패는 응답 코드·요청 대상·예외 종류까지 남깁니다.
경우 B — 크론이 아예 안 돌았다
이쪽이 더 안 보입니다. 작업 로그 자체가 없기 때문입니다. 없는 로그를 아무리 뒤져도 나올 게 없습니다.

| 원인 | 확인·조치 |
|---|---|
| 타이머가 꺼져 있음 | systemctl --user is-enabled 로 확인 후 다시 켬 |
| 그 시각에 서버가 꺼져 있었음 | Persistent=true 로 켜질 때 밀린 것을 실행 |
| 이전 실행이 안 끝나 겹침 | 잠금 파일로 중복 실행 방지 |
| 실행 경로·사용자가 달라 즉시 죽음 | WorkingDirectory·환경변수 확인 |
| 재부팅 뒤 등록이 풀림 | enable 이 돼 있는지 확인 |
마지막 줄을 겪었습니다. 임시로 띄워 둔 것은 재부팅하면 사라집니다. 그날 이후로 조용히 아무것도 안 하고 있었는데, 실행 기록을 볼 생각을 안 해서 한참 몰랐습니다.
저는 그때 작업 로그만 계속 뒤졌습니다. 로그가 비어 있으니 "아직 실행 전인가" 하고 넘겼습니다. 로그가 없다는 것 자체가 신호였는데 그렇게 안 읽었습니다.
대안으로 아예 enable 여부를 정기 점검에 넣어 두는 방법도 있습니다. 등록된 목록과 실제로 켜져 있는 목록을 대조해서, 빠진 게 있으면 알립니다.
알림을 갈라야 합니다
원인이 둘이면 알림도 둘이어야 합니다. 안 그러면 받을 때마다 처음부터 조사해야 합니다.
if not ran_recently(job):
notify(f"[미실행] {job} — 마지막 실행 {last_run(job)}")
else:
notify(f"[실행됐으나 실패] {job} — {last_error(job)}")
문구에 어느 쪽인지와 마지막 실행 시각만 넣어도 조사 시간이 크게 줄어듭니다. 저는 이걸 바꾸고 나서 알림만 보고 어디를 볼지 바로 정할 수 있게 됐습니다.
미실행은 결과로만은 못 잡습니다
여기가 핵심입니다. "결과물이 없다"로는 미실행을 못 잡습니다.
| 판정 방식 | A 감지 | B 감지 |
|---|---|---|
| 결과물이 있나 | 됩니다 | 됩니다(늦게) |
| 실행 기록이 있나 | — | 됩니다(바로) |
| 둘 다 본다 | 됩니다 | 됩니다 |
작업이 시작조차 안 하면 실패 알림을 보낼 코드도 안 돕니다. 그래서 미실행은 작업 바깥에서 감시해야 합니다. 안에서 스스로를 감시할 수는 없습니다.
저는 하루 한 번 도는 별도 점검에서 각 작업의 마지막 실행 시각을 훑고, 주기보다 오래됐으면 알리게 했습니다.
확인 못 한 것
겹침 때문에 실행이 건너뛰어졌을 때, 시스템이 그걸 어디에 남기는지는 확인하지 못했습니다. 잠금 파일로 막는 쪽으로 처리해서 더 파보지 않았습니다. 기록이 남는다면 그걸 바로 보는 편이 더 나을 것 같은데, 확인은 못 했습니다.
판정 — 계속 쓴다
원인을 둘로 나누는 것만으로 조사 시간이 줄었습니다. 코드 몇 줄이고, 알림을 볼 때마다 어디를 볼지 바로 정해집니다.
미실행 감시는 따로 둬야 합니다. 작업 안에 넣으면 그 작업이 안 돌 때 같이 안 돕니다. 바깥에서 봐야 합니다.
다만 감시하는 쪽도 안 돌 수 있습니다. 그래서 그쪽은 "확인했다"는 기록을 남기게 해두고, 그 기록이 끊기면 의심합니다.
직접 하실 분께
하나. 실패 알림에 "돌았나 안 돌았나" 를 반드시 넣으세요. 이 한 줄이 조사 방향을 정합니다.
둘. 마지막 실행 시각을 알림에 같이 넣습니다. 언제부터 안 돌았는지가 바로 보입니다.
셋. 미실행 감시는 작업 바깥에 둡니다. 안에서 자기를 감시할 수 없습니다.
넷. 임시로 띄운 것은 재부팅하면 사라집니다. 정식으로 등록했는지 확인하는 항목을 점검에 넣어 두세요.
다음에는 실행이 겹쳐서 건너뛰어진 경우를 기록으로 바로 확인할 방법이 있는지 찾아보려고 합니다. 되면 이어서 적겠습니다.