게이트가 198번 작동했습니다. 저는 그것을 '정상 작동'이라 불렀습니다.
요약
코딩 에이전트 구축 과정에서 발생한 '게이트(gate)' 시스템의 오탐 문제를 다룹니다. 차단 횟수라는 수치적 지표가 시스템의 성능을 나타내는 진정한 성공 지표가 될 수 없음을 경고하며, 검증 도구 자체의 오류 가능성을 지적합니다.
핵심 포인트
- 차단 횟수(firing rate)와 시스템의 유효성은 별개의 문제임
- 검증 도구가 수락 기준을 충족한 코드를 거절하는 오탐(false positive) 주의
- 시스템 검증 도구 자체가 고장 날 수 있음을 인지해야 함
- 단순 수치가 아닌 명시적 수락 기준(acceptance criteria) 기반의 검증 필요
차단 횟수가 성공 지표가 될 수 없는 이유
ForgeFlow 시리즈의 일부 — M5 Max에서 실행 루프를 로컬로 실행하는 코딩 에이전트(coding agent)를 구축하며, 실제로 무엇이 고장 나는지 기록합니다. 계획 수립은 Claude에서 수행하며, 코드 생성은 Ollama를 통해 로컬 모델에서 실행되고, Docker 샌드박스 내부에서 테스트 주도(test-driven) 방식으로 진행됩니다.
저는 나쁜 코드를 차단하기 위한 게이트(gate)를 만들었습니다. 이 게이트는 198개의 코드를 차단했고, 저는 그 숫자를 게이트가 잘 작동하고 있다는 증거로 받아들였습니다.
그 후 차단된 사례들을 열어보고, 각 사례가 해당 작업의 수락 기준(acceptance criteria)을 충족하는지 하나씩 대조하며 읽어보았습니다. 그중 상당수는 나쁜 코드가 아니었습니다. 게이트가 충분히 자주 틀렸기 때문에, 저는 더 이상 차단 횟수를 게이트가 잘 작동한다는 증거로 읽을 수 없었습니다. 게이트는 설계된 대로 끊임없이 작동(firing)하고 있었고, 저는 "많이 작동한다"는 것을 "제 역할을 하고 있다"로 착각했던 것입니다. 이 두 가지는 같은 말이 아닙니다.
이 글은 이 에이전트를 구축하는 동안 제가 계속해서 걸려 넘어졌던 문제에 관한 짧은 연재의 두 번째 포스트입니다. 즉, 시스템을 검증하기 위해 사용하는 도구 자체가 고장 날 수 있으며, 그것들은 종종 성공처럼 보이는 방식으로 고장 난다는 점입니다. 지난 포스트는 통과함으로써 거짓말을 한 테스트 실행에 관한 것이었습니다. 이번 포스트는 차단함으로써 거짓말을 한 게이트에 관한 것입니다.
게이트란 무엇이며, 왜 나는 그 수치를 신뢰했는가
이 에이전트는 테스트 주도 루프(test-driven loop) 내에서 작동하며, 그 루프의 한 단계가 바로 게이트입니다. 특정 코드가 통과하기 전에, 게이트는 해당 코드가 표준을 충족하는지 확인하고, 충족하지 못하면 코드를 차단하고 작업을 다시 수행하도록 되돌려 보냅니다. 게이트는 취약하거나 형식이 잘못된 시도가 커밋(commit)되는 것을 방지하기 위해 존재합니다.
한동안 게이트의 주요 수치는 무언가를 차단한 횟수, 즉 198회였습니다. 저는 그것을 보고 기분이 좋았습니다. 그 논리는 명백해 보였습니다. 게이트가 많은 나쁜 시도를 잡아내고 있으니 게이트는 가치 있고, 따라서 게이트가 있음으로써 시스템은 더 건강해진다는 것이었습니다. 높은 차단 횟수, 열심히 일하는 게이트, 더 적은 나쁜 커밋. 굳이 더 자세히 들여다볼 필요가 있을까요?
그러한 추론에는 허점이 있으며, 이 포스트는 바로 그 허점에 대해 다룹니다.
내가 조용히 하나로 합쳐버린 두 가지 주장
차단된 사례들을 살펴봤을 때 — 단순히 횟수가 아니라 실제 사례들을 — 게이트가 차단한 것 중 상당 부분이 괜찮은 작업이었다는 사실을 발견했습니다. 형식이 잘못되었거나(malformed) 품질이 낮은 것이 아니었습니다. 게이트가 인식하지 못하는 형태를 띠었을 뿐인, 정당한 시도들이었기에 거절당한 것이었습니다.
여기서 "괜찮다(fine)"는 말이 무엇을 의미하는지 정확히 짚고 넘어가고 싶습니다. 왜냐하면 "게이트가 틀렸다"라는 주장이 성립하려면 기준점(ground truth)이 필요하며, 그렇지 않으면 그것은 그저 제 개인적인 의견에 불과하기 때문입니다. 여기서 기준점(ground truth)이란 "내가 그 코드를 좋아했다"는 뜻이 아닙니다. 각 단계마다 명시적인 수락 기준(acceptance criteria)이 있었다는 뜻입니다. 즉, 통과해야 할 대상 테스트, 생성해야 할 동작, 그리고 해당 단계에 명시된 제약 조건들을 의미합니다. 차단이 _오탐(false positive)_이었던 경우는 이러한 기준들이 이미 충족되었음에도 불구하고, 작업의 성공 조건에 포함되지 않는 속성을 이유로 게이트가 작업을 거절했을 때를 말합니다.
예를 들어: 통과해야 할 모든 테스트를 통과했지만, 내부 구조가 게이트의 검사(check)가 예상한 것과 일치하지 않는다는 이유로 거절된 솔루션이 있었습니다. 즉, 작업 자체에서는 요구하지 않았던 표현 방식(representation)을 사용했을 뿐, 작업 수준의 동작은 동일했던 경우입니다. 이것은 취향의 문제가 아닙니다. 작업은 기준을 충족했습니다. 게이트는 그 기준 밖에 있는 이유로 거절한 것입니다.
따라서 198번이라는 숫자는 실제였습니다. 그 차단 사례들은 모두 실제로 일어난 일입니다. 틀렸던 것은 제가 그 숫자에 부여했던 의미였습니다. 저는 서로 다른 두 가지 주장을 하나로 합쳐버렸던 것입니다:
- 게이트가 작동했습니다. (사실입니다. 198번. 검증 가능합니다.)
- 그 작동은 정당했습니다. (확인된 적이 없으며 — 결과적으로, 정당하지 않은 경우가 많았습니다.) "무언가를 차단했다"는 사실과 "그 무언가를 차단한 것이 옳았다"는 사실은 독립적인 사실입니다. 게이트는 매우 활발하게 작동하면서 동시에 매우 잘못될 수 있습니다. 그리고 잘못 보정된 (miscalibrated) 게이트는 두 가지 특성을 모두 갖는 경향이 있는데, 왜냐하면 좋은 작업물을 거부하게 만드는 동일한 결함이 많은 양의 작업물을 거부하게 만들기 때문입니다. 제가 가치의 증거로 취급했던 차단 횟수는, 단순히 너무 쉽게 트리거를 작동시키는 (trigger-happy) 게이트의 결과와도 똑같이 일치합니다. 차단 횟수만으로는 정당한 거부와 거짓 양성 (false positive)을 구분할 수 없습니다.
이것은 가드 (guards) — 린터 규칙 (linter rules), CI 체크, 검증 레이어 (validation layers), 정책 필터 (policy filters) — 를 구축할 때 저지르기 쉬운 실수처럼 보입니다. 적어도 제 경우에는 그랬으며, 우리가 인정하고 싶은 것보다 더 흔한 일일 것이라고 의심합니다. 대시보드는 당신에게 활동성 (activity) 을 보여줍니다. 활동성은 보호받고 있다는 느낌을 줍니다. 하지만 활동성이 곧 올바른 활동성은 아니며, 대시보드는 대개 그 차이를 알지 못합니다. 그래서 대시보드는 당신에게 위안을 주는 숫자를 보여주고, 당신이 스스로를 치켜세우는 해석을 덧붙이게 내버려 둡니다.
"그렇다면 어떻게 무언가가 통과될 수 있었나요?"
그것은 타당한 질문이며, 마침내 제가 제대로 들여다보게 만든 질문이기도 합니다. 만약 게이트가 대부분의 시간 동안 잘못되었다면, 어떻게 시스템이 조금이라도 진전을 이룰 수 있었을까요?
답은 불편합니다. 시스템은 게이트 덕분이 아니라, 게이트 불구하고 진전을 이루었습니다. 전형적인 실패 사례는 다음과 같았습니다. 첫 번째 시도는 테스트를 통과했지만 게이트가 신뢰하지 않는 구조를 사용했기에 차단되었습니다. 재시도는 동작을 개선한 것이 아니라, 단지 게이트가 수용할 수 있는 형태로 동일한 동작을 재구성했을 뿐이었습니다. 대시보드 관점에서는 이것이 "게이트가 개선을 강제했다"로 보였습니다. 하지만 사례 검토 (case review) 관점에서는 그것은 게이트에 대한 적응이었습니다. (심지어 그 적응조차 작동하지 않을 때면, 저는 직접 개입하여 작업을 통과시켰습니다. 왜냐하면 그것이 괜찮다는 것을 볼 수 있었기 때문입니다 — 이는 게이트가 제 역할을 다하지 못하고 있다는 또 다른 조용한 신호였습니다.)
그것이 제가 놓쳤던 신호였습니다. 실제로 제 역할을 하는 게이트(gate)라면 루프가 '더 나은' 코드로 수렴하게 만듭니다. 하지만 제 게이트는 루프를 '게이트 모양을 닮은' 코드로 수렴하게 만들고 있었습니다. 이는 전혀 다른 문제이며, 때로는 더 나쁩니다. 재시도(retry)는 코드를 더 정확하게 만들지 못했습니다. 그저 게이트가 받아들이기에 더 적절한 상태로 만들 뿐이었습니다.
이제 제가 진짜 블록과 가짜 블록을 구분하는 방법
이 문제를 포착하면서, 저는 정당한 블록(justified block)과 노이즈가 섞인 블록(noisy block)을 실제로 구분할 수 있는 기준을 적어 내려가야만 했습니다. 횟수는 분명 정답이 아니었습니다. 제가 도달한 결론은 세 가지 단계의 체크리스트입니다. 우아한 방식은 아니지만, 이후로 여러 사례를 잡아냈기에 규칙이라기보다는 이 설정에 대한 작동 가능한 휴리스틱(heuristic)으로 제안하고자 합니다.
1. 총합이 아니라 이유의 분포를 보십시오. 블록이 발생하는 이유가 작업의 질과 상관없이 동일한 얕은 표면적 특징에 반복적으로 걸려드는 것이 아니라, 실질적인 결함(substantive defects)에 매핑되어야 합니다. 만약 이유들이 후자(표면적 특징)에 뭉쳐 있다면, 게이트는 품질을 판단하는 대신 잘못된 것에 대해 패턴 매칭(pattern-matching)을 하고 있을 가능성이 높습니다. (이는 좁은 목적의 단일 체크보다는 광범위한 품질 게이트에 더 유용합니다.)
2. 재시도 시 무엇이 일어나는지 관찰하십시오. 이것이 가장 유용한 신호임이 밝혀졌습니다. 제 루프에서, '정당한' 블록은 해당 결함이 실제로 해결될 때까지 재시도 전반에 걸쳐 동일한 근본 결함에 작업이 머무르는 경향이 있었습니다. 반면 '거짓 양성(false positive)'은 동작을 개선하지 않으면서 표면만 바꾸는 형태 변화(shape-shifting) 시도들을 만들어냈습니다. 이는 법칙은 아니며 경향성일 뿐입니다. 결함이 실제 상황이라도 모델이 방황할 수 있기 때문입니다. 하지만 재시도 시퀀스의 형태는 단일 블록 이벤트가 담지 못하는 정보를 포함하고 있었습니다.
3. 최종 수렴 여부를 확인하십시오. 정당한 블록은 결국 '해결(resolve)'되어야 합니다. 즉, 작업이 다시 작성되고, 실제 문제가 수정되며, 그 자체의 장점으로 통과되어야 합니다. 만약 차단된 작업이 결코 수렴하지 않거나, 게이트를 약화시킨 후에야 겨우 "통과"한다면, 게이트가 틀렸거나 혹은 게이트는 맞았지만 당신의 루프가 그에 대응할 수 없다는 뜻입니다. 두 경우 모두 문제이며, 단순히 게이트가 몇 번 작동했는지만 세어서는 이 문제들을 발견할 수 없습니다.
이 중 어느 것도 그 자체로는 명확한 합격/불합격(pass/fail) 기준이 되지 않습니다. 하지만 이 요소들을 종합함으로써, 저는 그동안 간과해 왔던 질문, 즉 '이 차단(block)이 정당했는가?'라는 질문을 던질 수 있게 되었습니다. 단순히 차단이 발생했다는 사실에서 답을 읽어내는 대신 말입니다.
실수의 더 깊은 버전
이 문제의 밑바닥에는 더 일반적인 함정이 있으며, 저는 저 자신도 모르게 그 함정에 빠졌기에 이를 분명하게 명시하고자 합니다.
단순히 "게이트가 잘못된 입력(bad input)을 차단하는가"만을 확인하는 테스트는 쉬운 절반만을 테스트하는 것입니다. 어려운 나머지 절반은 다음과 같습니다: "게이트가 단지 특이하게 보일 뿐인 올바른 입력(good input)을 통과시키는가?" 만약 당신이 가드(guard)에게 거부해야 마땅한 입력들만 계속해서 제공한다면, 당연히 가드는 그것들을 거부할 것이고 테스트는 당연히 통과할 것입니다. 하지만 이는 가드의 오탐(false-positive) 동작에 대해서는 아무것도 증명하지 못하며, 바로 그 지점이 제 게이트가 실패하고 있었던 지점이었습니다. 게이트 자체의 테스트가 통과(green)로 나왔던 이유는 게이트가 건강해 보였던 이유와 동일했습니다. 제가 게이트에게 오직 듣기 좋은 질문만을 던졌기 때문입니다.
그래서 이제 저는 게이트를 테스트할 때, 정당하지만 형태가 기묘한 케이스들 — 즉, 게이트가 순진하게 불신할 수도 있는 형태의 유효한 작업들 — 을 의도적으로 포함하여 게이트가 그것들을 통과시키는지 확인합니다. 이 경우, 부정적인 케이스(나쁜 것을 거부하는 것)가 더 쉬운 절반이었습니다. 제가 충분히 테스트하지 못했던 위험 요소는 낯선 옷을 입고 있는 올바른 데이터들이었습니다.
이것이 증명하지 못한 것
저는 이 문제를 "모든 게이트는 나쁘다"라거나 "차단 횟수는 의미가 없다"는 식으로 부풀리고 싶지 않습니다. 둘 다 사실이 아닙니다.
게이트는 제 역할을 다할 수 있습니다. 잘 보정된(well-calibrated) 게이트는 실제로 진짜 문제들을 막아내며, 차단 횟수는 완벽하게 유효한 운영적(operational) 신호입니다. 즉, 무언가 일어나고 있다는 것을 알아차리거나 갑작스러운 급증(spike)을 포착하는 데 유용합니다. 실수는 횟수를 추적한 것이 아니었습니다. 차단 횟수를 단지 _활동(activity)_의 증거일 뿐임에도 불구하고, 그것을 _정확성(correctness)_의 증거로 취급한 것이 문제였습니다. 이 둘은 서로 다른 축이며, 저는 이 둘을 혼동했습니다.
또한 저의 세 단계 점검 방식은 이 특정한 시스템, 즉 차단된 작업이 자동으로 재시도되는 테스트 주도 루프(test-driven loop)에 의해 형성되었다는 점을 말씀드리고 싶습니다. 덕분에 저에게는 "재시도 횟수를 주시하라"는 것이 하나의 신호로서 활용될 수 있었습니다. 만약 여러분의 설정이 이러한 종류의 궤적(trajectory)을 생성하지 않는다면, 이 내용의 일부는 적용되지 않을 것입니다. 저는 이것을 가드(guard)에 관한 일반적인 정리(theorem)가 아니라, 한 곳에서 효과가 있었던 사례로서 제안하는 것입니다. 저는 이 프로젝트를 진행하며 일반성(generality)에 대해 틀렸던 적이 있기에, 이 의견을 조심스럽게 제시합니다.
솔직하게 밝히는 핵심 요약 (The takeaway)
가드가 무언가를 차단한다는 것은 그것이 _작동(active)_하고 있다는 사실을 알려줄 뿐입니다. 그것이 _옳다(right)_는 것을 알려주지는 않습니다. 이 둘은 별개의 사실이며, 그 사이의 간극이야말로 자신감 있어 보이는 게이트가 좋은 작업을 처벌하면서 그것을 보호라고 부르는 소음 생성기(noise machine)로 조용히 변질될 수 있는 지점입니다.
만약 여러분이 게이트, 린터(linter), 검증기(validator), 정책 필터(policy filter) 등 무언가를 중단시키는 도구를 운영하고 있다면, 단순히 차단된 총량만이 아니라 _차단된 항목의 샘플(sample)_을 감사(audit)해 볼 가치가 있습니다. 총량은 쉽게 성실함처럼 보일 수 있습니다. 하지만 그 성실함이 진짜인지 확인하는 곳은 바로 샘플입니다.
다른 분들은 이를 어떻게 다루는지 궁금합니다. 게이트나 엄격한 CI 규칙을 운영하신다면, 차단된 내용이 정당했는지 확인하기 위해 샘플을 추출해 보신 적이 있나요? 만약 그렇다면, 주관적으로 흐르지 않으면서 어떻게 "정당함"을 결정하시나요? 저는 제 상황에 맞는 대략적인 방법을 찾아냈지만, 이는 제 시스템의 특정한 형태에 의존하고 있습니다. 다른 곳에서는 어떻게 이루어지는지 듣고 싶습니다.
시리즈 다음 편: 저는 AI 에이전트에게 무언가를 세어달라고 요청했습니다. 에이전트는 12라고 답했습니다. 실제 숫자는 13이었고, 그 단 한 번의 차이가 제가 현재 따르고 있는 규칙을 바꾸어 놓았습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기