정기 작업 118개, 모르는 사이에 오래된 코드를 실행하고 있었다
요약
AI 에이전트에게 버그 수정을 맡겼으나, 정기 작업(cron)이 실행하는 코드가 최신 버전과 다른 오래된 코드였던 사례를 다룹니다. 이는 개발 환경(편집 나무)과 실제 운영 환경(실행 나무)의 코드 불일치 문제이며, 자동화 시스템 설계 시 주의해야 할 중요한 함정을 제시합니다.
핵심 포인트
- 개발/운영 환경 분리 시 동기화 절차에 오류가 발생할 수 있습니다.
- AI 에이전트의 테스트 통과 보고만으로는 실제 작동 여부를 보장할 수 없습니다.
- 시스템의 '완료'는 개별 기능 존재(단계 1)를 넘어, 최종 실행 측면(단계 3)에서 검증되어야 합니다.
안녕하세요, 미오입니다. 사람 한 명과 AI 에이전트로 작은 회사를 운영하고 있습니다.
AI 에이전트에게 "이 버그를 고쳐줘"라고 부탁했고, "고쳤습니다"라는 답변을 받았습니다. 그런데 다음 날, 같은 버그가 또 발생했습니다. 이번 이야기는 그런 내용입니다.
AI가 거짓말을 한 것은 아닙니다. 테스트도 통과했었습니다. 다만, 밤의 정기 작업이 호출한 것은 수정 전의 코드였습니다.
이 글에서는 그 원인과, 수정한 것이 실제로 작동하는 곳에 도달했는지 확인할 수 있는 시스템을 정리합니다. 주기적인 자동화나 AI 에이전트를 운영하시는 분이라면, 같은 함정에 빠져 있을지도 모릅니다.
참고로, 경위는 가독성을 위해 일부 단순화했지만, 발생한 사건의 골격은 그대로입니다. 장소를 나타내는 이름이나 경로는 숨기거나 EDIT_TREE와 같은 임시 이름으로 처리했습니다.
무슨 일이 일어났나
코드의 위치가 두 군데 있었다
제 환경에는 코드가 놓이는 곳이 실질적으로 두 군데 있었습니다. 여기서는 '나무'(디렉토리 구조)라고 부릅니다.
- 편집 나무 (編集の木): AI가 파일을 읽고, 고치고, 테스트하는 장소입니다. 버전 관리도 여기에 있습니다. -
- 실행 나무 (実行の木): 정기 작업(cron에 해당하는 스케줄러)이 실제로 호출하는 곳입니다.
처음에는 같은 장소였습니다. 운영을 계속하면서 안전을 위해 실행 쪽을 분리했습니다. 편집 중인 깨진 상태를 밤의 정기 작업이 가져가지 않도록 하기 위해서입니다. 이 판단 자체는 틀리지 않았습니다.
문제는, 편집 나무에서 실행 나무로 반영하는 절차가 사람과 AI의 기억에 의존하고 있었다는 것입니다.
"고쳤다"가 거짓은 아니었다
어떤 버그 수정 작업을 AI에게 맡겼습니다. AI는 편집 나무에서 원인을 특정하고, 수정을 가했으며, 테스트를 통해 "수정했습니다"라고 보고했습니다. 편집 나무 내에서는 확실히 고쳐져 있었습니다.
다음 날, 같은 버그가 발생했습니다. 로그를 보니, 수정 전의 동작만 남아 있습니다. 정기 작업이 호출하는 파일의 실체를 조사해 보니, 실행 나무에 있던 것은 수정 전의 코드였습니다.
조사를 확장하자, 동일한 유형의 불일치는 한 곳만이 아니었습니다. 정기 작업을 점검해 보니, 총 118개가 있었습니다. 그중, 실행 나무의 코드가 편집 나무의 최신 버전과 일치하지 않는 것이 다수 발견되었습니다. 밤마다 오래된 코드가 정상 종료 상태로 돌아가고 있었던 것입니다.
왜 아무도 눈치채지 못했나
이유는 세 가지였습니다.
- 정기 작업은 에러를 내지 않고 종료하고 있었습니다. 오래된 코드는, 오래된 방식으로 작동하기 때문입니다. -
AI의 완료 보고는 "편집 나무에서 테스트가 통과했다"라는 사실에 근거했습니다. 보고 자체는 정확했고, 어긋난 것은 확인한 장소 쪽이었습니다. - 저 역시 "고쳤다고 하니 고쳐졌다"라고 받아들이고 있었습니다.
확인하는 대상과 실제로 작동하는 대상이 달랐던 것입니다. 누군가 게을렀다기보다는, 시스템에 구멍이 있었던 것입니다.
"되었다"를 세 단계로 나누다
사고 이후, "되었다"라는 단어를 세 단계로 나누어 다루게 되었습니다.
| 단계 | 표현 | 확인 내용 | 확인 방법 |
|---|---|---|---|
| 1 | 부품이 존재함 | 파일이나 함수가 존재하며, 개별적으로 작동함 | 편집 나무에서의 테스트 |
| ... | |||
| 사고 시점에서 충족된 것은 단계 1뿐이었습니다. AI도 저도, 단계 1을 "완료"라고 부르고 있었던 것입니다. |
중요한 것은, 단계 3의 증거는 실행 측에서만 얻을 수 있다는 것입니다. 편집 나무를 아무리 조사해도, 단계 3은 알 수 없습니다.
이후로는 완료라고 말할 수 있는 것은 단계 3까지 확인했을 때뿐으로 했습니다. 단계 1이나 2에 머물러 있다면, "부품은 존재하지만 아직 작동하지 않는다"고 그대로 적게 합니다.
재발 방지 1: 반영을 절차서가 아닌 커맨드로 만들기
나무를 하나로 되돌리면 이야기가 빠르지만, 분리한 이유가 있기 때문에 돌아갈 수 없습니다. 그래서 반영을 일방향/일 커맨드로 했습니다.
#!/usr/bin/env bash
# deploy.sh — 편집 나무 → 실행 나무. 이 외의 경로로 실행 나무를 건드리지 않기
set -euo pipefail
...
포인트는 세 가지입니다.
- 미커밋이면 중지. 무엇을 작동시켰는지, 커밋 식별자로 나중에 추적할 수 있도록 하기 위해서입니다. -
데이터와 로그는 제외. 실행 나무에만 존재하는 상태를,--delete로 지우는 사고를 피하기 위해서입니다. -
이후 시스템의 토대가 됩니다.VERSION을 실행 나무에 작성합니다.
실행 나무를 손으로 고치는 것도 금지했습니다. 급하게 수동으로 개입하고 싶은 상황은 반드시 있습니다. 그래도, 편집 나무에서 고치고 deploy.sh
を通します。例外を一度認めると、ずれはそこから再発するからです。
재발 방지 2: 틀어짐을 기계로 감지하기
반영을 커맨드로 해도, 실행을 잊는 일은 발생합니다. 그래서, 틀어짐을 언제든지 조사할 수 있도록 했습니다.
#!/usr/bin/env bash
# tree_drift.sh — 두 개의 트리의 차이를 냄. 차이가 있으면 종료 코드 1
set -uo pipefail
...
이것을 정기적으로 돌리고, 종료 코드가 1일 때 알림을 보냅니다. 알림을 보내는 방법은 감시 시스템 이야기로 넘어가니, 여기서는 생략하겠습니다. 핵심은, 차이를 기계로 낼 수 있는 상태로 만들어 두는 것입니다.
VERSION
도 비교합니다. 실행 트리에 적힌 식별자가 편집 트리의 최신 커밋과 다르면, 미반영된 수정이 있다는 뜻입니다.
cd "$EDIT_TREE"
head_rev="$(git rev-parse --short HEAD)"
run_rev="$(cut -d' ' -f1 "$RUN_TREE/VERSION")"
...
재발 방지 3: 실행 로그에 버전을 새기기
여기까지는 단계 2 이야기입니다. 단계 3의 '작동하고 있음'을 확인하려면, 실행하는 쪽이 자신의 버전을 밝혀야 합니다. 그래서 각 정기 작업의 시작 부분에 버전만 내보내는 공통적인 장치를 넣었습니다.
# stamp.py — 각 정기 작업의 시작 부분에서 호출
import os, sys, time
def stamp(job_name: str) -> None:
...
로그를 검색하는 것만으로 다음을 알 수 있습니다.
- 어젯밤 실행이 어떤 버전의 코드로 돌아갔는지.
- 수정을 반영한 후,
첫 번째 실행에서 새로운 버전이 사용되었는지. -
UNKNOWN
이 보인다면, 애초에 실행 트리 외부에서 호출되었다는 뜻입니다.
'수정했다'와 '고쳐져서 작동했다' 사이에는, 다음 정기 실행을 한 번 지켜보는 과정이 있습니다. AI가 이 과정을 건너뛰지 못하게 하기 위해, 완료 보고의 형태를 바꿨습니다.
재발 방지 4: 완료 보고에 '작동 증거' 항목 만들기
AI의 완료 보고를 자유 기술로 하는 것을 멈추고, 형식으로 만들었습니다. 단계 3에서 어디까지 확인했는지를 AI 자신이 채웁니다.
## 완료 보고
- 대상: (무엇을 고쳤는지. 1줄)
- 단계 1 부품이 있음: ○/× 근거: (테스트 이름과 결과)
...
운용 규칙은 세 가지입니다.
- 단계 3가 '미도래'인 건은, 완료 목록에 넣지 않고,
확인 대기 목록에 둡니다. - 확인 대기는 다음 실행 후에, 보는 사람(AI든 사람이든)이 근거를 채우고 닫습니다. - 확인 대기가 오래 남으면, 그것 자체를 감시 대상이 됩니다.
이렇게 되니, 보고 내용이 '열심히 했습니다'에서 '어느 단계까지 확인했는지'로 바뀝니다. AI에게 '솔직하게 보고해 달라'고 부탁할 필요도 없어졌습니다. 형식의 항목에 근거를 채워야 하므로, 형식이 정직함의 대용을 해줍니다.
118개의 정리 방법
사고 후 뒷정리로, 먼저 정기 작업의 재고 조사를 했습니다. 한 일은 간단해서, 정기 작업이 호출하는 스크립트를 열거하고, 어떤 트리를 가리키는지에 따라 분류합니다.
crontab -l | grep -v '^\[\s]*#' | grep -oE '/[^ ]+\.(sh|py)' | sort -u |
while read -r f; do
case "$(realpath "$f" 2>/dev/null || echo "$f")" in
...
분류는 세 가지입니다.
- RUN: 실행 트리를 가리키고 있습니다. 올바른 형태로, 틀어짐 감지 대상이 됩니다. -
- EDIT: 편집 트리를 직접 호출하고 있습니다. 편집 중인 깨진 상태를 건질 위험이 있으므로, RUN으로 변경합니다. -
- OTHER: 트리 외부의 경로를 호출하고 있습니다. 존재 이유를 확인하여, 어느 한쪽으로 옮기거나, 예외로 대장에 기록합니다.
변경은 기계적으로 진행하고, 하나씩 다음 실행 로그에서 stamp을 확인했습니다. 시간은 걸립니다. 그래도 '바꾼 척'하는 것은 첫 번째 사고와 같은 유형의 실패입니다. 여기서 확인을 생략하면, 같은 구멍에 다시 빠집니다.
함정과 피하는 법
실행 트리를 직접 고쳐버리기
급할 때 실행 트리의 한 줄을 손으로 고치면, 다음 deploy.sh
로 덮어쓰여 사라집니다. 반대로 deploy.sh
가 --delete
를 사용하지 않으면, 편집 트리에 없는 파일이 실행 트리에 계속 남아 있게 됩니다. 실행 트리는 읽기 전용에 가까운 취급을 하고, 손으로 만진 흔적을 알 수 있도록, 틀어짐 감지를 계속 돌려주세요.
테스트가 '존재'만 보고 있다
편집(Edit)의 나무에는 테스트가 있습니다. 2단계와 3단계는 별도의 확인이 필요합니다. 아무리 테스트를 늘려도 이 구멍은 메워지지 않습니다.
정상 종료를 성공으로 읽는다
오래된 코드는 옛날 방식대로 정상 종료됩니다. 종료 코드 0은 '작동했다'라는 것만 알려줄 뿐, 어떤 버전으로 작동했는지는 로그의 버전에서 읽어야 합니다.
설정의 나무도 별로 있다
코드뿐만 아니라 설정 파일이나 환경 변수에도 두 개의 나무가 있을 수 있습니다. 설정도 반영 커맨드의 대상에 포함할지, 아니면 설정의 버전도 stamp에 표시해 주세요.
자신의 환경에서 확인하는 3가지 절차
마지막으로 오늘부터 시도할 수 있는 세 가지 확인 사항을 제시합니다. 경로는 자신의 환경에 맞게 변경해 주세요.
1. 정기 작업이 어느 위치를 가리키는지 계산하기
앞서 언급한 재고 조사(棚卸し) 커맨드를 자신의 경로로 수정하여 실행합니다. 편집할 장소와 정기 작업이 부르는 장소가 같다면, 이 사고는 일어나지 않습니다. 다르다면, RUN / EDIT / OTHER의 개수를 확인합니다. OTHER가 나오면, 먼저 이유를 확인해 주세요.
2. 두 위치의 차이를 한 번만 가져오기
diff -rq --exclude='.git' --exclude='data' --exclude='logs' "$EDIT_TREE" "$RUN_TREE"
출력이 있다면, 그것이 '지금 이 순간, 오래된 코드가 작동하고 있을지도 모르는 파일'입니다. 차이가 없다면, 적어도 2단계는 충족됩니다.
3. 최근에 '수정했다'고 말한 건의 3단계 확인하기
최근 수정 사항을 하나 골라, 다음 실행 로그에서 새로운 버전이 나오는지 확인합니다. stamp 같은 장치가 아직 없다면, 실행 로그에서 '그 수정을 넣은 후 출력 내용이 바뀌었는지'를 찾아봅니다. 찾지 못했다면, 확인할 메커니즘 자체가 없다는 것을 알 수 있습니다. 그것을 아는 것만으로도 수확입니다.
여유가 된다면, 완료 보고서의 형식을 AI에게 전달하여 다음 요청부터 1단계·2단계·3단계의 근거를 채우게 하세요. '미도래(未到来)'를 솔직하게 쓰게 할 수 있다면, 시스템이 기능하고 있다는 뜻입니다.
맺음말
'존재하는 것'과 '작동하는 것' 사이의 구멍은 주의만 한다고 메워지지 않습니다. 반영을 커맨드로 만들고, 차이를 기계로 감지하며, 실행하는 쪽에게 자신의 버전을 명시하게 해야 합니다. 그렇게 시스템으로 막습니다.
이 이야기를 포함하여 1인과 AI 에이전트로 회사를 운영하는 데 필요했던 운용 유형들을 책에 정리했습니다. 업무를 배차표로 잘라 흘려보내는 시스템이나, 멈춘 것을 감시하는 시스템도 같은 생각으로 쓰고 있습니다. 『AIだけで回す個人の会社 ― 配車票・検証台・見張りで、Claude を頭脳だけに使う運用術』입니다. 이 기사의 원본이 된 제3장과 제1장은 무료로 읽을 수 있습니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기