성공했다고 거짓말을 하는 클린업 스크립트
요약
정규 표현식 이스케이프 오류로 인해 아무 작업도 수행하지 않고 성공 메시지만 출력하는 클린업 스크립트 버그 사례를 소개합니다. 코딩 에이전트를 활용하여 버그를 재현하고, 단순히 코드를 수정하는 것을 넘어 실제 디스크 용량이 줄어드는 것을 증명하며 문제를 해결하는 과정을 다룹니다.
핵심 포인트
- 실패를 명확히 알리지 못하는 필터는 잘못된 성공을 보고할 위험이 있음
- 정규 표현식 작성 시 불필요한 이스케이프 처리가 매칭 실패를 유발할 수 있음
- 코딩 에이전트를 활용해 운영 이슈의 근본 원인을 찾고 해결 과정을 검증 가능
- 수정 사항은 단순히 코드 변경이 아닌 실제 결과(디스크 용량 감소)로 증명해야 함
제가 크래시(crash)보다 더 두려워하게 된 특별한 종류의 버그가 있습니다. 바로 아무것도 하지 않으면서 성공했다고 보고하는 버그입니다.
크래시는 적어도 어디가 아픈지는 알려줍니다. 하지만 이 버그는 당신을 향해 미소 짓습니다. 매일 밤 실행되는 클린업(cleanup) 작업이 실행되어 cache cleanup complete.를 출력하고 종료 코드 0(exit zero)을 반환하지만, 디스크는 여전히 가득 차 버립니다. 무엇을 삭제할지 결정해야 하는 라인이 단 하나의 항목도 매칭하지 못했기 때문입니다. 매 실행은 성공 메시지를 달고 있는 '아무 작업도 하지 않음(no-op)' 상태였습니다. 로그를 읽어서는 이를 찾아낼 수 없습니다. 로그 자체가 문제입니다. 로그는 거짓말을 하고 있으며, 심지어 초록색(성공)으로 거짓말을 하고 있습니다.
이 버그의 구체적인 형태는 일단 파악하고 나면 거의 우스꽝스럽기까지 합니다. 경로 매칭(path-matching) 도우미 함수가 정규 표현식(regex)을 생성하는 과정에서 슬래시(/)를 이스케이프(escape) 처리했습니다. 즉, /를 \/로 바꾼 것입니다. 사용자 입력을 정제(sanitizing)한다는 이론 하에 말이죠. 하지만 /는 정규 표현식의 메타 문자(metacharacter)가 아닙니다. 이를 이스케이프 처리함으로써 경로에 리터럴 백슬래시(\)가 포함되어야만 하는 패턴이 생성되었고, 실제 .cache/jobs/... 경로에는 그런 백슬래시가 절대 나타나지 않습니다. 결과적으로 "이 파일이 관리 디렉토리 아래에 있는가?"라는 체크는 영원히 모든 항목에 대해 false를 반환했습니다. 클린업 스크립트는 충실하게 아무것도 남기지 않고, 아무것도 삭제하지 않은 채, 완료를 선언했습니다.
제가 계속해서 다시 배우고 있는 교훈은 이것입니다: 크게 실패(fail loudly)할 수 없는 필터는 영원히 당신에게 정중하게 거짓말을 할 것입니다. 어떤 매처(matcher)를 신뢰하기 전에, 확실히 매칭되는 값을 입력해 보고 제대로 작동하는지 확인하세요. 한 번도 빨간색(실패)으로 변하는 것을 본 적 없는 도구에서 나온 초록색(성공) 결과는 통과(pass)가 아닙니다. 그것은 예의 바른 태도를 가진, 테스트되지 않은 추측일 뿐입니다.
저는 이를 재구축하고 에이전트를 풀어놓았습니다
단순히 이야기만 들려드리는 대신, 저는 자주 보지 못하는 일을 했습니다. 버그가 의도적으로 심어진 작은 캐시 관리자(cache-janitor)를 임시 저장소(throwaway repo)에 재구축하여 버그를 재현했고, 코딩 에이전트(coding agent)를 아무런 정보 없이 투입했습니다. 버그가 어디에 있는지에 대한 힌트도, 스크립트도, 편집된 재촬영본도 없었습니다. 오직 운영상의 불만 사항인 "클린업은 성공했다고 뜨는데 디스크는 계속 가득 찬다"라는 정보만 주었습니다. 버그는 무대에 세팅되었고, 사냥은 시작되었습니다.
전체 세션은 아래에 포함되어 있습니다. 에이전트가 저장소(repo)를 읽고, 실패를 재현하며, 과도하게 이스케이프된 슬래시(over-escaped slash)를 격리하여 수정하고, 마지막으로 — 가장 중요한 부분인 — 이 모든 문제를 일으켰던 성공 메시지를 믿는 대신 캐시가 실제로 줄어드는 것을 보여줌으로써 수정 사항을 _증명_하는 과정을 지켜보세요. 그 마지막 단계가 이 이야기의 핵심 교훈이며, 기계가 스스로 그 결론에 도달하는 모습은 매우 만족스럽습니다.
자신의 성공에 대해 거짓말을 한 클린업 스크립트를 사냥하는 AI 에이전트 claude-sonnet-5
사용자 (You)
운영 보고(Ops report): 이 저장소의 야간 캐시 클린업(nightly cache cleanup)은 매 실행 시마다 'cache cleanup complete.'를 로그에 남기지만, .cache 볼륨은 계속 커지고 있으며 이번 달에만 디스크가 두 번 가득 찼습니다. 근본 원인을 찾아 수정하고, 캐시가 줄어드는 것을 보여줌으로써 수정 사항이 실제로 작동함을 증명하세요. 'npm run seed'를 실행하여 현실적인 2주간의 캐시 상태를 재현할 수 있습니다.
에이전트 (Agent)
먼저 저장소를 탐색하여 캐시 클린업 스크립트를 찾고 그 로직을 이해하겠습니다.
에이전트 (Agent)
▸ Bash ls -la && cat package.json 2>/dev/null
입력 (Input)
$ ls -la && cat package.json 2>/dev/null
출력 (Output)
total 28
drwxr-xr-x 3 root root 4096 Jul 14 08:13 .
drwxr-xr-x 3 root root 4096 Jul 14 08:14 ..
...
에이전트 (Agent)
▸ Read /work/janitor/janitor.js
입력 (Input)
/work/janitor/janitor.js
출력 (Output)
1 // janitor.js — nightly cache cleanup. Reads the job manifest, removes artifact
2 // directories that are (a) under the managed cache prefix and (b) older than MAX_AGE_DAYS.
3 // Runs from cron via `npm run clean`.
...
에이전트 (Agent)
▸ Read /work/janitor/seed.js
입력 (Input)
/work/janitor/seed.js
출력 (Output)
1 // seed.js — dev helper: simulate two weeks of nightly job runs so the cache
2 // looks like production (a mix of stale and fresh artifacts + the manifest).
3 const fs = require("fs");
...
에이전트 (Agent)
▸ Read /work/janitor/README.md
입력 (Input)
/work/janitor/README.md
출력 (Output
1 # cache-janitor
2
3 Nightly cleanup for the CI build cache. Job artifacts land in .cache/jobs/<job-id>/
...
$ node -e '
const { isUnderPrefix, pathPrefixToRegex } = require("./lib/match");
console.log(pathPrefixToRegex(".cache/jobs/"));
...
Output
/^\/\.cache\/jobs\//
true
Agent
이제 정규표현식 (Regex)이 올바르게 매칭됩니다. 현실적인 2주간의 캐시 (cache) 상태를 시드 (seed)하고, 클린업 (cleanup)이 실제로 캐시 크기를 줄이는 것을 보여드리겠습니다.
Agent
▸ Bash rm -rf /work/janitor/.cache npm run seed echo "--- before cleanup ---" du -sh...
Input
$ rm -rf /work/janitor/.cache
npm run seed
echo "--- before cleanup ---"
...
Output
> cache-janitor@1.2.0 seed
> node seed.js
...
Agent
▸ Bash npm run clean echo "--- after cleanup ---" du -sh .cache ls .cache/jobs | wc ...
Input
$ npm run clean
echo "--- after cleanup ---"
du -sh .cache
...
Output
> cache-janitor@1.2.0 clean
> node janitor.js
...
Agent
캐시가 3.9M/15개 작업 디렉토리 (job dirs)에서 1.9M/7개 작업 디렉토리로 줄어들었습니다. 06번 작업(7일 미만)만 유지하고 714번 작업(오래된 작업, stale)은 제거(pruning)했습니다. 수정 전이라면, 이 동일한 실행 과정에서
lib/match.js에서 불필요한 슬래시 이스케이프(slash-escaping)를 제거했습니다 — 실제 정규 표현식 메타 문자(regex metacharacters)만 이스케이프가 필요합니다.
증명 (Proof)
npm run seed를 통해 15개의 작업 디렉토리(0~14일 경과)를 생성했으며,.cache크기는 3.9M였습니다.- 이제
npm run clean을 실행하면 7일이 지난 모든 항목을 올바르게 제거(pruning)합니다:.cache가 1.9M로 감소했고, 작업 디렉토리가 15개에서 7개로 줄어들었습니다 (0~6일 차 작업만 남음). - 수정 전에는 동일한 명령어를 실행해도 15개의 디렉토리가 그대로 남아 있으면서 동일한 성공 로그를 출력했습니다 — 이는
isUnderPrefix를 직접 테스트하여 확인했습니다 (isUnderPrefix가 이전에는false를 반환했으나, 수정 후에는true를 반환함).
또한, 해결 사항을 기록하기 위해 README.md의 "알려진 문제(Known issue)" 섹션을 업데이트했습니다.
더 보기
19 / 19 메시지
이 글에서 한 가지만 기억한다면: 여러분의 코드베이스에서 가장 무서운 줄은 에러를 던지는(throws) 줄이 아닙니다. 모든 것이 괜찮다고 말하는 줄입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기