에러보다 '조용히 멈추는 것'이 더 위험하다
당사는 기사 공개, 매출 취득, 영업 파이프라인 등 정기적인 작업을 Mac의 launchd를 이용해 실행하고 있습니다. 도입을 고려하는 분들로부터 자주 듣는 질문은 "AI에 맡겼는데, 만약 고장 나면 어떻게 하느냐"입니다.
제 대답은 "고장 나는 것 자체는 그리 무섭지 않습니다. 무서운 것은, 고장 났다는 사실을 아무도 알아차리지 못하는 것입니다."입니다.
2026년 9월, 당사에서는 다음 세 가지가 아무도 눈치채지 못한 채 방치되고 있었습니다.
- Zenn에 배포 실패. push는 성공했지만 기사가 공개되지 않은 상태가 3일 지속됨
- note 기사의 중복 공개
- KDP 매출 취득 작업 미실행
만약 에러로 인해 다운되었다면 로그에 ERROR가 남습니다. 하지만 이번 세 건은 모두 '에러가 발생하지 않았거나' 혹은 '애초에 실행되지 않은' 유형이었습니다. 에러를 모니터링하고 있어도, 실행조차 되지 않은 작업은 아무것도 기록을 남기지 않기 때문에 감지할 수 없습니다.
본 기사는 이러한 문제에 당사가 어떻게 대처했는지 구현 기반으로 작성한 내용입니다.
- 정기 모니터링 (Watchdog): 멈춘 작업을 매일 포착합니다. 사람도 AI도 보고 있지 않은 시간대의 담당자 역할 -
- Claude Code의 Monitor 도구: 작업 중인 세션에서 '끝났는지/실패했는지'를 현장에서 즉시 받습니다.
도입 여부를 판단하는 입장의 분들을 위해, 각각 무엇을 맡기고 무엇은 맡기지 말아야 하는지 정리합니다.
판단 기준: 무엇이 달라지는가
먼저 결론부터 말씀드립니다.
| 관점 | 모니터링 없음 | 모니터링 있음 |
|---|
| 멈춤을 인지할 때까지 | 수일 (매출 집계나 독자의 지적으로 발각) | 최대 1일 (정기 모니터링) / 즉시 (세션 중) |
| ... | | |
당사의 수익은 노출에 크게 좌우됩니다. 기사 공개가 3일 동안 멈추면, 그 3일간의 유입은 나중에 되돌릴 수 없습니다. 모니터링 비용은 '하루 한 번 실행하는 Python 스크립트'와 '세션 중 몇 줄의 명령어'에 불과하기 때문에, 비용 대비 효과는 논할 필요조차 없었습니다.
맡길 것: 멈춤 감지, 에러 라인 추출, 알림 전송, 대기 시간 모니터링.
맡기지 않을 것: 멈춘 원인에 대한 최종 판단과 외부적인 재실행(재공개/재전송 등). 후자는 중복 공개 사고가 발생했던 영역이므로, 감지까지는 자동화하고 재실행은 사람이 승인합니다.
구현 1: 스케줄을 기준으로 '신선도'로 판별하는 Watchdog
처음에 만든 것은 '로그가 24시간 동안 업데이트되지 않으면 이상하다'라는 단순한 판단이었습니다. 이것은 곧 무너졌습니다. 매월 1일에 실행되는 작업은, 24일 전의 로그라도 정상이기 때문입니다.
그래서 각 작업의 스케줄을 launchd의 plist에서 읽어와, 해당 작업에게 허용 가능한 경과 시간(許容経過時間)을 산출하는 방식으로 변경했습니다. 아래는 당사에서 실제로 작동하고 있는 auto-watchdog.py
def job_schedules():
"""plist에서 {작업명: 허용되는 최대 경과 시간}을 만듭니다."""
out = {}
...
설계상의 포인트는 다음 세 가지입니다.
- 스케줄의 이중 관리를 하지 않는다. 모니터링용으로 '이 작업은 매일'이라고 별도로 작성하면, 작업을 추가할 때마다 모니터링 측의 업데이트를 반드시 잊게 됩니다. plist를 진실로 간주합니다. -
- 배열 스케줄의 함정.
[{Weekday:3,Hour:10},{Weekday:5,Hour:10}]
은 '하루 2회'가 아니라 '수요일/금요일 주 2회'입니다. 초기 구현은 이를 하루 2회로 해석하여 목요일에 매주 오보를 발생시켰습니다. -
- 정지와 에러를 분리해서 계산한다. 로그 업데이트 시간으로 '멈춤의 의혹'을,
-error.log 파일의 ERROR|실패|Traceback으로 '에러 출력'을 판별합니다. 앞서 언급한 세 건 중 두 건은 전자로만 포착할 수 있었습니다.
이상 징후가 있으면 Slack Webhook으로 메시지를 보내고 종료 코드 1로 빠져나옵니다.
if stale or errors:
notify_slack(":rotating_light: [작업 모니터링] 멈춤 또는 에러를 감지했습니다\n```\n" + report + "\n```")
return 1
함정: 로그 이름과 작업명이 어긋날 때
note-publish처럼 stdout을 별도의 파일로 리다이렉션(流出)하는 작업은, 작업명만으로 기계적으로 로그를 특정할 수 없습니다. 당사는 대응표를 가지고 있으며, 특정할 수 없는 작업은 '로그 미특정 (검사 대상 제외)'으로 보고서에 명시적으로 포함하도록 하고 있습니다. 조용히 건너뛰면, 모니터링 자체가 '보이지 않는 실패'가 되어버리기 때문입니다.
LOG_ALIAS = {
"note-publish": ".company/scripts/note/post-note.log",
"retry-zenn-deploy": None, # 일회성 복구 작업. 상주하지 않음
...
의도적으로 중지된 작업은 이유와 날짜를 명시하여 제외 목록에 남깁니다. 이는 6개월 후에 "왜 감시하지 않았는지" 아무도 설명할 수 없게 되는 것을 방지하기 위함입니다.
검증 방법: 일부러 오래되게 만들어서 작동하는지 확인하기
감시는 '이상 없음'이 계속 출력되면, 실제로 작동하고 있는지 알기 어렵습니다. 도입했다면 반드시 한 번은 의도적으로 이상을 만들어 보세요.
# 일일 작업 로그를 3일 전 시각으로 수정
touch -t "$(date -v-3d +%Y%m%d%H%M)" .company/scripts/logs/auto-morning.log
python3 .company/scripts/auto-watchdog.py; echo "exit=$?"
...
확인한 후에는 다음 작업 실행 시 로그가 업데이트되면서 원래대로 돌아옵니다.
구현 2: 세션 중에는 Monitor에 "기다리는" 역할을 맡기기
Watchdog은 일일(daily) 주기이므로, 감지 지연이 최대 하루까지 발생합니다. 반면, Claude Code로 작업하는 도중에 '푸시한 글이 정말로 공개되었는지', '긴 빌드가 끝났는지' 등은 그 자리에서 알고 싶습니다.
여기서 사용하는 것이 바로 Claude Code의 Monitor 툴입니다. Monitor는 백그라운드에서 명령을 실행하고, 표준 출력(standard output)에 한 줄씩 나올 때마다 그것을 Claude에게 알림으로 전달합니다. Claude가 sleep하며 폴링(polling)할 필요 없이, 무언가 발생했을 때만 반응할 수 있습니다.
당사에서 가장 효과적이었던 것은 앞서 언급한 Zenn 배포 실패의 재발 방지였습니다. 푸시를 하면 공개 URL이 200을 반환할 때까지 Monitor로 기다립니다.
# 공개 URL이 200이 되거나, 15분이 경과하면 한 줄을 출력하고 종료한다
url="https://zenn.dev/joinclass/articles/<slug>"
for i in $(seq 1 30); do
...
Claude Code에게 자연어로 "푸시한 글이 공개될 때까지 Monitor로 감시해 달라"고 요청하기만 하면 충분합니다. NOT_PUBLISHED 라인이 도착하는 시점에 Claude는 실패를 파악하고, 재배포 절차(당사에서는 retry-zenn-deploy.sh)를 제안합니다. 앞서 설명했듯이, 실행할지 여부는 사람이 결정합니다.
로그 감시에도 사용할 수 있습니다.
tail -F .company/scripts/logs/auto-sales-pipeline.log \
| grep --line-buffered -E "ERROR|失敗|Traceback"
함정(落とし穴)
- 파이프를 거치는
grep은 버퍼링(buffering)되기 때문에, 에러가 발생해도 알림이 몇 KB 쌓일 때까지 출력되지 않습니다. 'Monitor가 울리지 않는다 = 정상'이라고 오해하는 전형적인 예입니다. grep --line-buffered를 잊으면 아무것도 도착하지 않습니다.
- 성공만 기다리지 않기. 위의 예시처럼
exit 0으로만 끝나는 구조라면, 실패 시에는 아무것도 출력되지 않고 무음 상태로 종료됩니다. 반드시 타임아웃(timeout)을 설정하여 "실패했다"는 한 줄을 출력해야 합니다. 이 부분을 누락하면, 감시 자체도 다시 '조용히 멈추는' 것이 됩니다.
- 출력 필터링하기.
tail -F를 그대로 흘려보내면 모든 라인이 알림이 되어 Claude의 컨텍스트(context)를 불필요하게 소모합니다. 이상한 라인만 출력하도록 필터링해야 합니다.
사용처 요약 비교
| Watchdog (launchd 일일)
| Monitor (세션 중)
| |
| :--- | :--- |
| 감시 시간대 | 아무도 보지 않는 야간/휴일 | 작업 중 몇 분~몇십 분
| ... |
결론: 모니터링은 "신뢰 비용"을 낮추는 투자
AI에게 업무를 맡기는 가장 큰 장벽은 성능보다 '맡겨도 괜찮을까'라는 불안이라고 저는 생각합니다. 그 불안의 정체는 대부분 '고장 났을 때 알 수 없는 것'입니다.
모니터링이 있으면, 멈춰도 하루 이내에 알 수 있습니다. 알 수 있다면, 맡길 범위를 넓힐 수 있습니다. 당사가 자동화 범위를 넓힐 수 있었던 것은 작업(job)의 수를 늘렸기 때문이 아니라, 멈췄다는 것을 감지할 수 있는 시스템을 먼저 만들었기 때문입니다.
오늘 해야 할 일은 두 가지입니다.
- 스케줄 기준에 따라 주기적 작업(periodic job)의 로그 업데이트 시간을 판정하는 스크립트를 하나 배치한다.
- 다음으로 push나 deploy를 할 때, Claude Code에게 "끝날 때까지 Monitor로 지켜봐 줘. 실패하면 알려줘"라고 요청한다.
이 기사에서 소개한 모니터링 설계(monitoring design)를 포함하여, Claude Code로 업무를 자율 운영하는 구현 노하우는 Zenn 도서에 체계적으로 정리되어 있습니다.
토론 (Discussion)