dev.to

How to stop AI from confidently shipping broken code (a pattern that actually works)

AI가 자신 있게 망가진 코드를 배포하지 못하게 하는 방법 — 실제로 작동한 패턴

AI에게 작성한 코드를 스스로 검토하라고 시키면 같은 확신을 반복할 뿐입니다. 글쓴이는 30일간 AI가 모든 애플리케이션 로직을 작성하게 한 뒤, 별도 컨텍스트의 다른 검토자에게 실패 시나리오만 찾게 하는 ‘Refutation Gate’를 제안합니다.

AI 요약

AI가 만든 코드가 테스트를 모두 통과하고 읽기에도 깔끔하더라도 운영 환경에서 고객에게 손해를 입힐 수 있습니다. 글쓴이는 30일 동안 애플리케이션 로직을 한 줄도 직접 작성하지 않고 AI에게 맡기면서, 어떤 절차가 이런 문제를 실제로 잡는지 확인했습니다. 답은 더 똑똑한 모델이나 정교한 프롬프트가 아니라 코드 작성자와 별개의 검토자가 실패를 증명하도록 만드는 구조였습니다.

■ 자기 검토는 검증이 아니라 확신의 반복입니다

AI에게 “이 코드가 올바른지 검토해 달라”고 요청하면 작성 과정에서 이미 코드가 맞다고 판단한 맥락을 그대로 이어갑니다. 모델은 자신의 작업을 조금 더 격식 있는 문체로 다시 평가하고, 기존 판단을 승인하는 방향으로 답하기 쉽습니다. “맞는지 확인하라”와 “어떻게 깨지는지 찾아라”는 서로 다른 작업인데, 전자는 모델이 거의 항상 수행하는 작업이라는 설명입니다.

글에서 제시한 규칙은 간단합니다. 두 번째 검토자가 코드를 깨뜨리려 시도하고 실패하기 전에는 어떤 변경도 병합하지 않습니다. 이 구조가 작동하려면 세 가지 조건이 필요합니다. 코드를 작성한 에이전트가 승인까지 맡지 않아야 하고, 검토자는 작성 과정을 보지 않은 깨끗한 컨텍스트에서 시작해야 하며, 가능하면 작성자와 다른 모델 계열을 사용해야 합니다.

작성자는 이미 수천 개의 토큰을 쓰며 코드가 맞다고 자신을 설득했기 때문에 같은 컨텍스트는 오염되어 있습니다. 새 컨텍스트의 검토자는 작성 과정을 기억하지 않으므로 최소한의 독립성을 확보합니다. 모델 계열까지 다르면 학습 데이터와 약점이 달라져 같은 함정을 함께 놓칠 가능성을 줄일 수 있다고 설명합니다.

■ “검토해 달라” 대신 실패 입력을 만들게 합니다

검토자에게 다음처럼 요청하지 않습니다.

“이 diff를 검토하고 올바른지 말해 주세요.”

대신 코드가 이미 깨졌다고 가정하고, 실패를 일으킬 구체적인 입력과 실행 순서, 상태를 만들라고 지시합니다. 네트워크가 최악의 순간에 끊기는 상황, 두 작업이 동시에 실행되는 상황, 외부 호출이 성공한 뒤 데이터베이스 쓰기가 실패하는 상황, 일반적인 사용자가 하지 않을 행동까지 조건으로 제시합니다. 목표는 데이터가 사라지거나 고객이 돈을 잃는 정확한 시나리오입니다. 정말 실패 사례를 찾지 못했다면 그 사실과 함께 어떤 조건이 충족되어야 안전한지도 설명하게 합니다.

이 방식은 검토자의 기본 방향을 “맞다”에서 “깨졌다”로 바꿉니다. 같은 모델과 같은 가중치를 사용해도 질문의 목적이 달라지면 출력이 달라질 수 있다는 주장입니다. 모델이 더 나은 코드를 작성하도록 요구하는 것이 아니라, 모델이 놓친 실패를 찾아내도록 역할을 제한합니다.

■ Stripe webhook 사례: 200 응답이 너무 빨랐습니다

글쓴이가 제시한 실제 diff는 Stripe webhook handler입니다.

``js app.post('/webhook', async (req, res) => { const event = verify(req); res.sendStatus(200); // Stripe에 수신 완료를 알림 await db.savePayment(event); // 그 뒤 데이터베이스에 저장 }); ``

코드는 짧고 깔끔합니다. Stripe에 즉시 200 응답을 보내 webhook 지연 시간을 낮추고, 테스트도 매번 통과합니다. AI에게 올바른지 묻자 낮은 지연 시간의 장점까지 칭찬하며 승인했습니다.

하지만 Refutation Gate의 실패 조건인 “외부 호출이 성공한 뒤 데이터베이스 쓰기가 실패하는 상황”을 적용하면 문제가 드러납니다. 200 응답과 savePayment 사이에 데이터베이스 장애가 발생하면 Stripe는 이벤트가 전달됐다고 판단합니다. 반면 내부 데이터베이스에는 결제 기록이 남지 않습니다. 결제한 고객은 상품이나 서비스에 접근하지 못하고, 결제 사실을 확인할 기록도 사라질 수 있습니다.

수정은 실행 순서를 바꾸는 한 줄입니다. 먼저 결제 정보를 저장하고, 저장이 끝난 뒤 Stripe에 응답해야 합니다. “검토해 달라”는 질문에서는 보이지 않던 문제가 “돈을 잃게 만드는 입력과 상태를 만들어라”라는 지시에서는 바로 드러났다고 설명합니다. 코드를 바꾼 것은 질문이 아니라, 실패를 찾도록 설계한 검토 절차입니다.

■ 모델만으로 닫히지 않는 사각지대

이 방식도 완전한 자동화 해법은 아닙니다. 글쓴이는 같은 webhook 코드를 동일한 모델 계열의 두 에이전트에게 전달하고, 두 번째 에이전트에 실패 시나리오를 찾으라는 지시를 내렸습니다. 그럼에도 검토자가 때때로 코드를 승인했습니다. 두 모델이 “깔끔한 webhook 코드”에 관한 비슷한 학습 사례를 접했고, 저장보다 먼저 승인하는 순서를 괜찮다고 판단했기 때문입니다. 검토자가 맹점을 반박하지 못하고 같은 판단을 더 자신 있게 재현한 셈입니다.

반면 특별한 프롬프트를 사용하지 않은 시니어 엔지니어는 5분 만에 “쓰기 전에 응답하고 있습니다. 2021년에 정확히 이런 문제로 호출을 받았고, 수습하기 악몽 같았습니다”라고 지적했습니다. 글쓴이는 모델이 사람보다 덜 똑똑해서가 아니라, 사람이 실제 장애를 겪으며 얻은 경험과 모델이 학습한 설명 사이에 차이가 있다고 봅니다. 그래서 최종 병합 버튼에는 여전히 사람이 있어야 합니다. 사람의 역할은 에이전트보다 코드를 잘 작성하는 데만 있지 않고, 과거에 어떤 실패가 실제 피해로 이어졌는지 기억하는 데 있습니다.

■ 바로 적용할 절차

  • AI에게 자신이 작성한 코드를 승인하라고 요청하지 않습니다. 승인은 AI가 쉽게 내놓는 출력이므로 검증 신호가 되지 않습니다.
  • 작성자와 분리된 컨텍스트에서 두 번째 검토자를 실행합니다. 가능하면 다른 모델 계열을 선택합니다.
  • “이 코드가 맞는가”가 아니라 “데이터나 돈을 잃게 만드는 정확한 입력과 순서를 만들어라”라고 지시합니다.
  • 최종 병합은 사람에게 맡깁니다. 특히 비슷한 장애를 직접 겪은 사람이 검토하면 모델의 공통 맹점을 보완할 가능성이 커집니다.

글쓴이는 이 구조를 자신이 만드는 서비스 Xenition의 형태로 설명합니다. 한 에이전트가 작업하고, 다른 에이전트가 전담으로 결과를 무너뜨리며, 사람이 병합을 책임지는 구조입니다. 다만 글의 관찰은 30일 동안 AI가 작성한 코드와 운영 위험을 지켜본 경험에 기반하며, 작성자는 같은 diff에 “검토해 달라”는 요청과 실패 유도 브리프를 적용한 A/B 측정, 서로 다른 모델 계열이 잡아내는 문제의 차이를 추가로 묻고 있습니다.

원문: dev.to / 번역·요약: Trawling