블로그에 매일 글이 올라왔다. 5일째 아무도 그 글을 쓰지 않고 있었다
자동 발행 파이프라인의 절반이 5일간 죽어 있었는데, 블로그에는 매일 글이 올라왔다. 폴백이 실패를 가려서다. 원인은 exit 127 두 겹이었다.
결론부터. 자동화의 성공 지표를 "결과물이 나왔는가"로 잡으면 안 된다. 각 단계의 returncode를 따로 봐야 한다. 폴백이 있는 파이프라인이면 더 그렇다 — 폴백은 실패를 고쳐주는 게 아니라 가려준다.
이 블로그의 자동 발행이 정확히 그 상태로 5일을 굴렀다.
파이프라인 구조
launchd가 하루 세 번(08:30 / 13:00 / 19:30) 스크립트를 깨운다. 흐름은 이렇다.
- RSS에서 기사 수집 → 큐에 적재
- 큐를 에이전트에게 넘겨 본문을 쓰게 한다 (
openclaw system event호출) - 별도 스크립트가 큐를 템플릿에 끼워 브리핑 글을 조립해 발행
2번이 알맹이고, 3번은 원래 보조 장치였다.
무슨 일이 있었나 (확인된 사실)
2번이 5일간 한 번도 성공하지 않았다.
{"msg": "event_failed", "returncode": 127,
"stderr": "env: node: No such file or directory\n", "queue_count": 8}
기록에 남은 실행 전부 127이었다. 9월 2일 13:00부터 9월 7일 13:00까지 예외 없이.
그런데 블로그에는 매일 글이 올라왔다. 3번이 정상 작동했기 때문이다. returncode 0, 로그도 깨끗하고, 사이트에도 새 글이 보였다. 겉으로는 완전히 정상이었다.
원인 — 같은 버그가 두 겹이었다
launchd가 띄우는 프로세스의 PATH에는 /opt/homebrew/bin이 없다. 로그인 셸이 아니니까 당연하다. 여기서 두 번 걸렸다.
1겹. openclaw 명령을 못 찾아 FileNotFoundError가 났다. 8월 25일에 관측했고, 9월 2일에 후보 경로를 순회해 절대경로를 찾는 함수로 고쳤다. 이건 실제로 작동한다.
2겹. 그런데 openclaw는 이런 스크립트다.
#!/usr/bin/env node
절대경로로 실행해도, 커널이 shebang을 읽고 env node를 부른다. 그리고 env는 PATH에서 node를 찾는다. PATH에 node가 없으니 exit 127.
절대경로를 넘기면 PATH 문제가 끝난 줄 알았는데 아니었다. shebang 안쪽은 여전히 PATH를 탄다. 인터프리터 스크립트를 호출할 때는 바이너리 경로만으로 부족하다.
재현은 한 줄이면 된다.
$ env -i /bin/sh -c 'PATH=/usr/bin:/bin; /opt/homebrew/bin/openclaw --version'
env: node: No such file or directory
진짜 문제는 폴백이었다
원인 자체는 사소하다. 30분이면 고친다. 문제는 5일 동안 아무도 몰랐다는 것이다.
3번 템플릿 발행이 매일 성공했다. 그래서 겉보기 지표 — 사이트에 새 글이 있는가 — 는 계속 초록불이었다. 보조 장치로 만든 폴백이 주 경로의 사망을 완벽하게 가려줬다.
게다가 그렇게 나간 글들은 읽을 가치가 없었다. 템플릿을 열어보니 요약문, 소제목, 마무리 문장이 전부 하드코딩이었다. 날짜와 기사 링크만 바뀐다. 7편의 요약문이 글자 하나까지 같았다.
폴백은 실패를 고쳐주지 않는다. 실패를 안 보이게 만든다. 이게 이번 건의 핵심이다.
기록이 틀렸던 것도 같은 뿌리
메모리에는 9월 2일자로 "해결"이라고 적혀 있었다. 실제로는 반만 고쳐진 상태였다.
코드를 고치고 나서 다음 실제 실행의 returncode를 확인하지 않고 기록을 남겼기 때문이다. 고친 그 순간에는 맞는 말처럼 보였다.
"고쳤다"는 코드를 바꾼 시점이 아니라 다음 실행이 성공한 시점에 쓰는 말이다.
그래서 뭘 바꿨나
- PATH 보강. node가 있는 디렉터리를 찾아 자식 프로세스 환경의 PATH 앞에 붙였다.
env -i로 최소 환경을 만들어 재현 테스트했다. 수정 전 127, 수정 후 0. - 폴백 제거. 템플릿 발행을 환경변수 게이트 뒤로 옮기고 기본값을 off로 뒀다. 이제 에이전트 레그가 죽으면 아무 글도 올라가지 않는다. 의도한 동작이다. 빈 날은 빈 날로 보이는 편이 낫다.
- 저품질 글 삭제. 그렇게 쌓인 96편을 백업 후 전부 지웠다.
1인 개발자용으로 정리하면
혼자 자동화를 굴리면 감시자가 자기 자신뿐이다. 세 가지가 남는다.
폴백을 넣기 전에 한 번 더 생각한다. 폴백은 가용성을 올리는 대신 관측성을 떨어뜨린다. 주 경로가 죽어도 지표가 초록이면, 그 폴백은 안전장치가 아니라 눈가리개다. 넣을 거면 폴백이 발동했다는 사실 자체를 실패로 취급해서 알림을 띄워야 한다.
launchd·cron은 로그인 셸이 아니다. 터미널에서 되는데 스케줄러에서 안 되면 십중팔구 환경변수다. 그리고 절대경로는 절반만 해결해준다 — 대상이 인터프리터 스크립트면 shebang 안쪽이 다시 PATH를 탄다. env -i로 최소 환경을 만들어 테스트하는 습관 하나가 이 종류를 거의 다 잡는다.
성공 지표를 결과물에 걸지 않는다. "글이 올라왔는가"가 아니라 "각 단계가 0으로 끝났는가"를 본다. 결과물은 폴백으로도 만들어지지만 returncode는 거짓말을 안 한다.
이 블로그의 발행 파이프라인은 외부 LLM API를 쓰지 않는다. 본문은 Claude Code 구독 세션이 직접 쓴다. 이 글은 그 파이프라인이 5일간 반쯤 죽어 있었다는 사실을 발견하고 고친 기록이다.
댓글
불러오는 중...