에이전트 파이프라인에서 3주 동안 숨겨진 충돌을 발견하게 해준 조용한 폴백
요약
에이전트 파이프라인에서 예외를 조용히 처리하는 폴백(fallback) 메커니즘이 어떻게 3주 동안 버그를 은폐했는지 분석합니다. 잘못된 예외 처리와 저하된 출력(degraded output)이 시스템의 가시성을 어떻게 해치는지 설명합니다.
핵심 포인트
- 조용한 폴백은 오류를 즉시 인지하지 못하게 만들어 버그를 장기화함
- 그럴듯해 보이는 저하된 출력보다 명시적인 실패(fail loud)가 더 나음
- 잘못된 입력에 대해 입구(entry point)에서부터 가드 로직을 구현해야 함
- 예외 처리 시 스택 트레이스를 숨기기보다 문제의 근원을 명확히 드러내야 함
크론 작업(cron job)이 3주 동안 조용히 실패하고 있었습니다. 실행할 때마다 예외가 발생했고, 실행할 때마다 이를 포착했으며, 매번 실제 보고서 대신 '충분히 좋은' 일반 텍스트 알림으로 폴백했습니다. 아무도 알아차리지 못했는데, 그 이유는 폴백이 작동했기 때문입니다. 이것이 함정입니다: 실패는 캐치 블록을 트리거하기에 충분히 크고, 그 안에서 결코 표면화되지 않을 만큼 조용했던 것입니다.
실제 버그와 실제 수정 방법, 그리고 이 사건을 통해 배운 일반적인 규칙을 소개합니다.
사건 발생
일일 요약 스크립트는 보류 중인 항목들의 큐를 순회하며 리뷰 이메일을 작성했습니다. 대부분의 항목은 아직 렌더링을 기다리는 로컬 폴더(entry.dir)를 가리켰고, 일부는 이미 다른 곳에 업로드되어 url만 가지고 있었습니다. 코드는 다음 순서로 작동했습니다:
for (const entry of queue) {
const fullPath = path.join(MEDIA_ROOT, entry.dir); // (1) 항상 실행됨
if (entry.url) {
...
라인 (1)은 이 경우를 처리해야 하는 entry.url 검사보다 먼저 무조건 실행됩니다. 이미 업로드된 항목이 dir: undefined와 함께 큐에 들어오자, path.join()이 오류를 발생시켰고, 전체 루프가 중간 반복에서 죽었으며, 이를 감싼 래퍼(wrapper)가 예외를 포착하여 실제 리뷰 요약 대신 단순히 '문제가 생겼으니 수동으로 확인하세요'라는 알맹이 없는 이메일을 보냈습니다.
그 폴백 이메일은 괜찮아 보였습니다. 오류 페이지도 아니었고, 받은 편지함에 스택 트레이스(stack trace)가 있는 것도 아니었습니다. 단지 그 자리를 대체하기에는 너무 쓸모없었던, 정상적으로 보이는 메시지였을 뿐입니다. 그래서 url만 가진 항목이 들어올 때마다 계속해서 조용히 발생했고, 마침내 누군가
단 한 줄이었습니다. 핵심은 해결책이 아니라, 그것이 어디에서 발생했느냐 하는 점입니다. 증상을 숨기고 있던 try/catch가 아니라, 잘못된 값의 근원지에서 발생했다는 사실 말입니다. try/catch가 존재하는 것 자체가 틀린 것은 아닙니다. 잘못된 큐 항목 하나 때문에 전체 알림 작업이 중단되는 것을 원치 않는 것은 합리적입니다. 하지만 그럴듯해 보이는 저하된 출력(degraded output)을 생성하는 catch 블록은, 그냥 명시적으로 실패(fail loud)하는 블록보다 더 나쁩니다. 명시적인 실패는 당일에 바로 수정되지만, 그럴듯해 보이는 저하된 출력은 누군가 우연히 내용을 대조해 볼 때까지 방치되기 때문입니다.
동일한 버그의 두 번째, 더 작은 버전
같은 주에 발생한 다른 스크립트 사례입니다. notify.mjs CLI가 플래그 처리 없이 process.argv[2]를 이메일 제목으로 가져갔습니다. 누군가 사용법 텍스트를 기대하며 node notify.mjs --help를 실행했습니다. 대신 소유자에게 --help라는 제목의 메시지가 이메일로 발송되었습니다. 두 번이나 발송되었는데, 두 번째 실행은 첫 번째 실행이 왜 이상하게 보이는지 누군가 다시 확인했기 때문입니다.
이번에도 해결책은 진입점(entry point)에 가드(guard)를 하나 두는 것이었습니다:
const subject = process.argv[2];
if (!subject || subject.startsWith("-")) {
console.log("Usage: node notify.mjs \"<subject>\" \"<body>\"");
...
버그는 다르지만 형태는 같습니다. "하나의 가시적인 출력을 생성하는 것"이 유일한 임무인 스크립트는 입구에서부터 잘못된 입력을 거부해야 합니다. 잘못된 전송의 대가는 스택 트레이스(stack trace)가 아니라, 소음을 내서는 안 되는 유일한 채널에 스팸을 뿌리는 것이기 때문입니다.
규칙
자동화 시스템 내에서 아무것도 생성하지 않는 대신 어떠한 출력을 생성하는 모든 catch 블록을 grep으로 찾아내어 스스로에게 물으십시오. "만약 이 분기(branch)가 실행된다면, 하류(downstream)의 어떤 것이라도 이를 알아차리는가?" 만약 대답이 "아니오, 그냥 정상적인(비록 약간은 나빠졌을지라도) 실행처럼 보입니다"라면, 당신은 에러 핸들링(error handling)을 하고 있는 것이 아니라, 변장을 한 버그를 가지고 있는 것입니다.
이러한 종류의 문제를 3주라는 시간을 허비하기 전에 잡아낼 수 있는 세 가지 점검 사항입니다:
- 폴백 경로(fallback path)가 성공 경로(success path)와 구별되지 않게 두지 마세요. 실제 결과물보다 더 짧고 단순한 버전을 조용히 생성하는 대신, 저하된 출력물에 눈에 띄는 태그를 붙이세요 (예: "부분적 결과 — 14개 항목 중 2개 실패").
- 상수(constant)여야 하는 것을 카운트하세요. 작업이 보통 N개의 항목을 처리한다면, N을 로그로 남기고 그것이 조용히 N-1로 떨어질 때 경고를 보내세요. 조용한 하락은 당신이 얻을 수 있는 가장 빠른 신호입니다.
- 로직 깊숙한 곳이 아니라 경계(boundary)에서 검증하세요. 위의 두 버그 모두 "문자열을 가정하는 함수에 정의되지 않은(undefined) 값이 도달한" 경우였습니다. 가드(guard)는 잘못된 값을 전달할 수 있는 모든 호출자에게 흩어져 있는 것이 아니라, 데이터가 함수로 들어오는 시점에 있어야 합니다.
이 중 어느 것도 생소한 것이 아닙니다. 이는 에이전트(agent)가 존재하기 전부터 유효했던 "빨리 실패하고, 크게 실패하라 (fail fast, fail loud)"라는 조언과 동일합니다. 이것이 특히 자동화된 파이프라인을 계속 괴롭히는 이유는, 사람이 동일한 작업을 수행하는 것을 지켜보는 것처럼 아무도 파이프라인이 실행되는 것을 실시간으로 지켜보지 않기 때문입니다. 조용하고 그럴싸해 보이는 성능 저하는, 누군가 그 자리에 있어야 할 것이 없다는 사실을 찾아내기 전까지는 보이지 않습니다.
만약 어떤 종류의 상시 가동되는 에이전트 파이프라인(always-on agent pipeline)을 운영 중이라면, 무료 필드 가이드(field guide)에서 나머지 신뢰성 체크리스트를 확인하세요. 가입 없이 7가지 규칙과 바로 복사해서 사용할 수 있는 3가지 Claude Code 가드레일을 제공합니다: **penloomstudio.com/field-guide.html_.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기