#S2 루트 리포트: 하루 만에 다섯 명의 낯선 사람이 내 시스템을 감사했다
요약
시스템 개발 및 배포 과정에서 발견된 5가지 오류 사례와 교훈을 다룹니다. 단순한 상태 확인을 넘어 명시적인 증거(receipts)를 확보하는 것과 AI 에이전트의 잘못된 피드백 루프를 방지하는 방법론을 제시합니다.
핵심 포인트
- 상태 값(ok) 대신 구체적인 메타데이터를 포함한 증거(receipts)를 기록해야 함
- 배포 게이트가 실제 실행될 파일을 검증하는지 확인하는 것이 필수적임
- 캐시 버전 확인 시 로컬과 실시간 파일의 일치 여부를 직접 검증해야 함
- AI 에이전트의 '정중하지만 틀린' 오류 메시지는 심각한 디버깅 루프를 유발함
- 가드레일 검증 시 유효한 입력으로 퍼즈 테스트를 수행하여 거짓 거부를 방지해야 함
지난번에는 각 게시물마다 증거(receipts)를 보여드리겠다고 했습니다. 이번 것은 단 하루 동안의 기록입니다.
하루 동안, 다섯 명의 낯선 사람들이 제 시스템을 감사했습니다. 그리고 그들 중 누구도 자신이 그런 일을 했다는 것을 알지 못했습니다. 이들은 모두 피드에 달린 댓글이나 게시물이었고, 각각 한 문장이었습니다. 하지만 그 문장들이 제가 이미 출시했고 이미 신뢰하고 있던 무언가의 구멍을 정확히 가리키고 있었습니다. 저는 그 문장을 읽고, 구멍을 찾아내서 — 보통 한 시간 이내에 — 막았습니다. 저는 AI를 이용해 병원의 내부 도구를 만드는 물리 치료사이기 때문에 '막았다'는 것은 제가 실제로 무엇이 고장 나고 있었는지 설명하는 동안 AI와 저가 그것을 해결했다는 의미입니다. 다섯 문장, 다섯 개의 구멍, 다섯 개의 증거 기록입니다.
또한 이들은 하나의 아이디어가 다섯 벌의 코트를 입은 것과 같았습니다. 이에 대해서도 말씀드리겠습니다.
루트 #1: '괜찮음(fine)'과 증거 기록(receipt) 사이의 차이점
누군가 제가 떨쳐낼 수 없는 스케줄링된 작업에 대해 글을 썼습니다. 증거 기록은 결과에 대한 메타데이터가 아니라, 결과 자체의 일부여야 한다. 라벨은 사일런트하게 구식이 되기 때문입니다. 왜냐하면 그 라벨을 작성한 주체가 더 이상 진실이 아니게 되었는데도 떠나버렸기 때문입니다.
제 시스템의 건강 검진(health check)은 ok라고 말했습니다. 그것이 전부였습니다. 녹색 단어 하나였죠. 그래서 저는 이것을 증거 기록을 제출하도록 만들었습니다. 어떤 버전이 확인했는지, 언제 했는지, 데이터베이스가 실제로 답변하는 데 몇 밀리초가 걸렸는지, 그리고 제가 이상하게 자랑스러워하는 한 가지 필드가 있습니다 — 단순히 누락된 것이 아니라, 존재하며 null로 설정된 명시적인 '저하 없음(no degradation)'입니다. 이 차이는 아무것도 아닌 것처럼 들리지만 모든 것입니다: 필드가 누락되었다는 것은
누군가 출시 게이트(release gate)를 통과한 머신러닝 모델을 배포했는데, 이 모델이 받은 모든 입력에 대해 '중립적(neutral)'이라고만 답변했습니다. 이 게이트는 최종 내보내기 단계(final export step) 전에 모델을 테스트했기 때문에, 실제로 사용자가 실행할 파일은 전혀 검증하지 못한 상태였습니다. 그들의 말은 이랬습니다: 당신의 게이트가 업로드하는 파일을 정말로 검증하는지 물어봐라.
저도 더 어리석은 곳에서 같은 간극을 겪었습니다. 모바일 도구를 배포할 때, 사람들이 오래된 코드를 받지 않도록 캐시 버전(cache version)을 올립니다. 그리고 저는 그 버전 업데이트가 사용자에게 도달했다고 믿고 있었습니다. 그래서 사용자가 실제로 받는 실시간(live) 파일을 가져와 제가 구축한 파일의 버전과 비교하는 검사를 작성했습니다. 실행해 보니: 로컬 v514, 실시간 v514, 일치합니다. 이 검사가 제 역할을 하는 첫 순간은, 두 버전이 일치하지 않는 날이고, 그 사실을 한 주 후에 당황한 사용자로부터가 아니라 스크립트에서 듣게 된 때였습니다. 제가 검증하는 것과 배포하는 것이 마침내 같은 객체가 되었습니다.
Loot #3: 정중하게 틀린 메시지
어떤 사람이 AI 에이전트를 디버깅했는데, 이 에이전트는 동일한 유효 코드(valid code)를 세 번 재시도했습니다. 그 도구는 에이전트에게 그럴듯하지만 잘못된 오류(plausible-but-wrong error)를 건넸고, 이에 에이전트는 실제 문제가 아니었던 문제를 충실하게 '수정'하며 루프에 빠졌습니다. 그들의 말은 저를 완전히 멈추게 했습니다: 최악의 버그는 빨간 스택 트레이스(red stack trace)를 던지지 않습니다. 대신 정중하고 자신감 넘치지만 잘못된 메시지를 건네주어 반복적으로 실패하게 만듭니다. 그리고 그 밑에 깔린 해결책은 이렇습니다: 일반적인 유효 입력으로 가드레일(guardrails)을 퍼즈 테스트하라 — 모든 거짓 거부는 버그이다.
저는 보안 문제를 탐지하기 위해 제 코드를 읽는 스캐너를 가지고 있습니다. 저는 이 스캐너가 잘못된 코드를 잡아내는지 테스트해 왔습니다. 하지만 저는 이 스캐너가 정상적인 코드를 잘못 플래그(flag)하는지는 한 번도 테스트해 본 적이 없었습니다. 이는 제 AI 에이전트 중 하나가 이미 올바른 상태인 무언가를 "수정"하러 떠나게 만드는 바로 그 실패 유형입니다. 그래서 저는 이미 검증된 여섯 개의 코드 스니펫(snippet)을 심어 두었습니다: 이스케이프 처리된 출력(escaped output), 안전한 매개변수화된 쿼리(parameterized query), 해시된 비밀번호(hashed password) 등 제 규칙이 오작동할 법한 깨끗한 패턴들이었습니다. 실행해 보니: 잡아야 할 것은 모두 잡았고, 여섯 개 중 어느 것도 잘못 플래그하지 않았습니다. 다만 한 가지 스니펫 — 주석 안에 들어 있는 "eval("이라는 문자열 — 은 여전히 스캐너를 작동시키는데, 이제 이것은 놀라운 사건이 아니라 라벨이 붙은 알려진 사각지대(blind spot)가 되었습니다. 만약 제가 규칙을 강화했다가 정상적인 코드를 물고 늘어지기 시작한다면, 짜증 난 팀원이 발견하기 전에 이 테스트가 먼저 빨간불을 띄울 것입니다.
전리품(Loot) #4: 뒤늦게 도착하는 청구서
누군가 3만 달러의 비용을 발생시킨 클라우드 실수에 대해 글을 썼습니다. 트래픽이 코드상에서는 보이지 않게 조용히 비싼 경로로 라우팅되었고, 변경 사항을 수행했다는 사실을 아무도 기억하지 못할 몇 주 뒤에야 청구서가 날아온 사건이었습니다. 그들의 문장은 이렇습니다: 주니어들에게는 아무도 경고하지 않는다. 왜냐하면 비용은 원인과 분리되어 지연된 상태로 나타나기 때문이다.
저는 클라우드 인프라를 운영하지는 않지만, 그 양상은 보편적입니다. 위험한 문제는 에러를 일으키는 문제가 아니라, 지금은 괜찮아 보이지만 나중에 대가를 치르게 되는 문제입니다. 서서히 차오르는 디스크. 조용히 소진되는 할당량(quota). 그래서 저는 제 상태 확인(health receipt) 항목에 가장 겸손한 버전 — 남은 디스크 용량을 단순한 숫자로 표시하는 것 — 을 추가했고, 실제로 무언가 망가지기 훨씬 전인 10% 남았을 때 경고를 띄우도록 감시자(watcher)에게 설정했습니다. 이것은 당신을 구해주는 그날 밤 전까지는 사소한 정보처럼 읽히겠지만, 그날이 되면 파일에서 가장 가치 있는 한 줄로 읽힐 것입니다. 환경이 비용을 청구하기 전에 환경이 당신에게 무엇을 청구할지를 기록해 두는 것이 세상에서 가장 저렴한 보험입니다.
전리품(Loot) #5: 자신의 보고서를 읽는 감사인
이 사람은 제가 _그날 아침_에 만든 무언가를 망가뜨렸습니다. 그리고 그는 스쳐 지나가는 행인이 아니었습니다. 일주일 내내 의견을 주고받던 사람이었죠. 그는 "외부 체크(external check)를 추가하기만 하면 됩니다"라는 말 뒤에 숨겨진 함정을 지적했습니다. 자체 보고된 영수증(self-reported receipt)을 채점하는 외부 프로세스는 감사인(auditor)처럼 보일 뿐입니다. 그것은 외부에 서서, 피감사자가 자신에 대해 작성한 보고서를 읽습니다. 글자만 입력하고, 아무것도 실행하지 않으며, 통과(pass)해 버립니다. 그는 이것이 감사인이 아예 없는 것보다 더 나쁘다고 말했습니다. 왜냐하면 실제 스크립트가 뒤에 버티고 있는 상태에서 초록색 불(green light)을 만들어내기 때문입니다.
저는 그의 테스트를 제가 3시간 전에 그토록 만족스러워했던 전리품(Loot) #1, 즉 영수증에 적용해 보았습니다. 실패했습니다. 저의 와처(watcher)는 헬스 엔드포인트(health endpoint) 자체의 JSON을 읽고, 해당 엔드포인트가 자신에 대해 작성한 "database: true"를 신뢰하고 있었습니다. 만약 버그로 인해 해당 엔드포인트가 거짓을 말하게 된다면, 저의 와처는 고개를 끄덕이며 이를 정상(healthy)이라고 부를 것입니다. 그래서 저는 그의 문장 나머지 부분을 실행했습니다. 이제 와처는 엔드포인트의 주장을 읽는 대신, 데이터베이스에 대해 자체적인(own) 쿼리를 직접 실행합니다. 그리고 만약 두 정보가 일치하지 않는다면, 그 불일치 자체가 바로 알람이 됩니다. 감사인은 보고서를 채점하는 것을 멈추고 작업을 확인하기 시작했습니다. 결과(Receipt): 깨끗하게 실행되며, 직접 쿼리와 자체 보고가 이제 일치합니다. 이는 한쪽이 다른 쪽을 보증했기 때문이 아니라, 둘 다 검증되었기 때문입니다.
다섯 명 모두의 공통점
이들을 나란히 세워두면, 하나의 아이디어가 다섯 개의 코트를 입고 있는 것과 같습니다. "괜찮음"이라고 말하는 상태(status). 잘못된 파일을 테스트하는 게이트(gate). 자신의 오탐(false alarm)을 절대 확인하지 않는 스캐너(scanner). 다음 달에 숨어 있는 비용(cost). 자신의 보고서를 읽는 감사인(auditor). 이들 모두는 자신의 숙제를 스스로 채점하는 것들이었습니다. 그리고 모든 해결책은 동일한 움직임이었습니다: 확신(confidence)을 믿지 마세요; 검사 대상이 작성하지 않은 무언가로 검증(verify)하세요. 라벨 대신 영수증을 사용하세요. 빌드된 파일이 아닌 배포된 파일(shipped file)을 사용하세요. 심어진 입력값(planted input)을 사용하세요. 조기에 만기가 돌아오는 숫자를 사용하세요. 와처가 직접 실행하는 쿼리를 사용하세요.
저는 이 원칙에 스스로 도달한 것이 아닙니다. 이것이 하나의 문장이라는 것을 깨닫기 전까지, 단 하루 동안 다섯 명의 낯선 이들로부터 다섯 번이나 훔쳐온 것입니다.
문이 열려 있다면 그것은 강도가 아닙니다
이 과정이 정직하게 유지되는 이유는 다음과 같습니다. 이 모든 것이 일방향적이지 않다는 점입니다. 전리품 #5는 그날 아침 자신의 도구에서 동일한 사각지대(blind spot)를 발견했던 누군가로부터 얻은 것이었습니다. 그는 자신이 나에게 건네준 바로 그 테스트로 자신의 훅(hook)을 망가뜨렸고, 저 또한 그것으로 제 것을 망가뜨렸습니다. 전리품 #3의 저자는 제가 그날 오후 바로 그의 "유효한 입력값으로 퍼징(fuzz)하라"는 라인을 코드로 옮기는 것을 지켜보았고, 그렇게 말했다고 말해주었습니다. 한 줄을 가져가면, 한 줄을 남깁니다. 그것이 이 경제의 전부입니다.
피드(feed)를 방송 채널로 취급하는 것을 멈추면, 그것은 바로 이런 것이 됩니다. 모두의 도구가 작업대 위에 펼쳐져 있는 공유 워크숍이며, "그거 훔쳐 갈게요"라는 말은 우리가 감사를 표하는 방식일 뿐입니다.
그러니 — 감사합니다. 하루 동안 다섯 개의 영수증. 다시 가방이 가득 찼습니다.
이 글은 Stolen from the Feed — 비개발자가 dev.to에서 가져와 실제로 구축하는 것들의 일부입니다. From Zero to Ship의 자매 시리즈입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기