내 AI 프로젝트를 직접 감사하며 배운 프로덕션 코드 배포에 관한 교훈
요약
AI 프로젝트 배포 과정에서 발생한 심각한 버그들을 직접 감사하며 얻은 교훈을 다룹니다. 조용히 실패하는 코드(silent failure)의 위험성을 경고하며, 예외 처리와 가시성 확보의 중요성을 강조합니다.
핵심 포인트
- 조용히 실패하는 코드는 추적이 불가능해 큰 문제를 야기함
- 실패 시 반드시 알림이나 로그가 남도록 설계해야 함
- 인증 서비스 장애 시 'failing open' 방식은 보안 취약점이 됨
- 정상 경로(happy path) 외의 예외 상황에 대한 계획이 필수적임
두 개의 프로젝트, 13개의 심각한 버그, 그리고 도처에서 계속해서 나타난 하나의 패턴.
대부분의 엔지니어들은 사용자가 문제를 보고할 때 자신의 코드에 문제가 있다는 것을 알게 됩니다. 저는 그보다 더 일찍 알게 되었습니다. 다른 누구도 나서기 전에, 제 자신의 프로젝트 두 개를 다시 살펴보고 의도적으로 제대로 감사(Auditing)함으로써 말입니다.
두 프로젝트는 거의 모든 면에서 달랐습니다. GitHub Autopilot은 GitHub 웹훅(webhooks)을 통해 코드 리뷰와 PR 관리를 자동화하는 Flask 앱입니다. TokenMizer는 LLM API 호출을 가로채어 지속적인 메모리(persistent memory)를 부여하는 프록시(proxy)입니다. 하나는 웹훅 트래픽을 처리하고, 다른 하나는 모델 요청을 처리합니다. 실패의 언어가 서로 다르다고 생각할 수도 있습니다.
하지만 그렇지 않았습니다. 동일한 실패 패턴이 매번 다른 옷을 입은 채 두 프로젝트 모두에서 나타났습니다. 제가 발견한 것, 그리고 작동하는 코드와 실제로 배포하기에 안전한 코드 사이의 차이점에 대해 무엇을 배웠는지 공유하겠습니다.
패턴: 스스로를 알리지 않는 실패
GitHub Autopilot에서는 코드가 예외(exception)를 포착하고 아무런 조치도 취하지 않는—로그도 없고, 알림도 없이 그저 조용히 계속 진행되는—27곳을 발견했습니다. TokenMizer에서는 동일한 형태의 버그가 12개 파일에 걸쳐 조용한 충돌(silent crash) 패턴으로 나타났으며, 데이터가 이미 잘못될 때까지 아무도 알아차리지 못한 채 동시 요청이 메모리 상태를 순서에 어긋나게 읽거나 쓸 수 있는 비동기 경합 조건(async race condition)도 있었습니다.
이 중 어느 것도 코드를 뚫어지게 쳐다본다고 해서 잡을 수 있는 버그가 아닙니다. 이것들은 "이게 작동하는가?"라는 질문 대신 다른 질문을 던짐으로써 잡을 수 있는 버그들입니다. 올바른 질문은 이것입니다: "이것이 실패할 때—그리고 결국에는 실패할 것입니다—그 실패가 누군가에게 알려주는가?"
크게 실패하는 코드는 짜증스럽지만 정직합니다. 조용히 실패하는 코드는 작성하기는 편하지만 실행하기에는 위험합니다. 왜냐하면 추적할 수 있는 흔적도 없이 작은 문제들이 큰 문제로 누적되게 만들기 때문입니다. 두 프로젝트 전체에서 제가 수행한 모든 수정의 핵심은, 조용한 실패를 가시적인 실패로 전환하는 것이었습니다.
세 가지 구체적인 실패, 하나의 근본적인 습관
GitHub Autopilot의 인증 우회 (auth bypass): MCP 인증 확인 과정에서 예외(exception)를 스스로 포착하고 요청을 통과시켜 버렸습니다. 즉, 차단(closed)하는 대신 허용(failing open)하는 방식으로 작동한 것입니다. 느려진 인증 서비스가 의도치 않은 백도어(backdoor)가 되었습니다.
GitHub Autopilot의 속도 제한기 (rate limiter): IP당 요청 횟수를 추적하지만 오래된 항목을 전혀 정리하지 않았습니다. 이로 인해 남용을 막기 위한 방어 기제가 지속적인 트래픽 상황에서 앱을 충돌시킬 수 있는 메모리 누수 (memory leak)로 변했습니다.
TokenMizer의 설치 프로그램 버그: MCP 설치 프로그램의 엣지 케이스 (edge case)로 인해, 설치 시 기존 메모리 그래프와 병합하는 대신 사용자의 기존 데이터를 덮어쓸 수 있었습니다. 이는 일상적인 설정 코드처럼 보이는 곳에 숨어 있던 데이터 파괴 버그였습니다.
세 가지 서로 다른 서브시스템 (subsystem)에서 발생한 세 가지 서로 다른 결과 — 보안 취약점, 서비스 중단 요인, 데이터 손실 버그. 하지만 그 밑바닥에 깔린 공통된 습관을 보십시오. 세 가지 경우 모두 코드가 '해피 패스 (happy path, 정상 경로)'가 유지될 것이라고 가정했을 뿐, 그렇지 않을 때 어떤 일이 벌어질지에 대해서는 계획하지 않았습니다. 인증 확인은 인증 서비스가 계속 작동할 것이라고 가정했습니다. 속도 제한기는 트래픽이 제한된 범위 내에 머물 것이라고 가정했습니다. 설치 프로그램은 잃을 것이 아무것도 없다고 가정했습니다.
훌륭한 방어적 코드 (defensive code)는 해피 패스를 가정하지 않습니다. 대신 "상황을 알 수 없을 때 안전한 기본값 (safe default)은 무엇인가?"라고 묻습니다. 보안이나 사용자 데이터에 영향을 미치는 모든 사항에 대해, 안전한 기본값은 거의 언제나 '거부(deny), 진행하지 않음(don't proceed), 덮어쓰지 않음(don't overwrite)'입니다.
테스트는 결승선이 아닙니다 — 테스트는 수정 사항을 증명하는 방법입니다
버그를 찾는 것은 하나의 기술입니다. 버그가 실제로 수정되었음을 증명하는 것은 또 다른 기술이며, 마감 압박 속에서 건너뛰기 쉬운 부분입니다. GitHub Autopilot의 경우, 이는 각 원래의 실패 사례를 재현하는 타겟 테스트 (targeted tests)를 작성하는 것을 의미했습니다. 즉, 위조된 요청 헤더 (request header), 인증 서비스 타임아웃 (timeout), 여러 IP로부터의 지속적인 트래픽 등을 테스트하는 것입니다. 이 과정에서 전체 테스트 커버리지 (coverage)를 62%에서 76%로 높였으며, 834개의 테스트 스위트 (test suite) 중 654개의 테스트를 모두 통과시켰습니다. TokenMizer의 경우, 광범위한 리팩터링 (refactor)이 문제를 모두 해결했을 것이라고 가정하는 대신 9가지 이슈를 개별적으로 검증하는 것을 의미했습니다.
중요한 숫자는 커버리지 퍼센티지 (coverage percentage)가 아닙니다. 당신이 발견한 각각의 구체적인 실패 모드 (failure mode)가 다시 발생했을 때 이를 잡아낼 수 있는 테스트가 있는지 여부입니다. 타겟팅된 회귀 테스트 (regression tests)가 없는 커버리지는 허영 지표 (vanity metric)에 불과하며, 커버리지 추적 없는 타겟팅된 테스트는 무엇을 확인하지 않았는지 알 수 없음을 의미합니다. 당신에게는 두 가지 모두가 필요합니다.
이 내용을 기록한 이유
저는 이 버그들을 조용히 수정하고 그냥 넘어갈 수도 있었습니다. 아무도 감사를 요구하지 않았고, 어떤 사용자도 리포트를 제출하지 않았기 때문입니다. 그럼에도 제가 기록을 남긴 이유는 수정 행위 자체가 실제로 가치 있는 부분이 아니었기 때문입니다. 진정으로 가치 있는 부분은 서로 관련 없는 두 코드베이스 (codebases) 사이에서 패턴을 발견하고, 이를 일회성 정리 작업이 아닌 재사용 가능한 교훈으로 만들기 위해 기록하는 것이었습니다.
이것이 제가 모든 엔지니어에게 권장하는 습관이며, 솔직히 말해서 제가 개발만큼이나 기술 글쓰기를 즐기는 이유이기도 합니다. 버그 수정은 하나의 프로젝트를 돕지만, 명확하게 설명된 패턴은 그 이후에 당신이 접하는 모든 프로젝트를 돕습니다. 두 번의 감사와 13개의 버그를 통해 얻을 수 있는 단 한 가지 가치가 있다면, 그것은 바로 이것입니다. 진짜 문제를 찾아내는 질문은 "데모에서 작동하는가?"가 아닙니다. "내가 의존하고 있는 것이 제대로 작동하지 않을 때 어떤 일이 발생하는가?"입니다. 이 질문을 초기에 던지십시오. 그러면 사용자에게 영향을 미치기 전에 버그를 수정할 수 있습니다.
GitHub Autopilot과 TokenMizer는 모두 오픈 소스입니다: github-autopilot · tokenmizer.
저는 TechNova World에서 개발자 도구를 만들고, 그것들을 만드는 과정에서 무엇이 고장 나는지에 대해 글을 씁니다. 실제로 프로덕션 코드 (production code)를 배포하고 감사해 본 사람으로부터 나온 문서를 원하신다면, 함께 이야기해 봅시다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기