AI-Native 시리즈 · 에이전트형 엔지니어링
앱은 계속 “완료”라고 말했다. 거짓말이었다. 그래서 우리는 앱이 스스로를 감사하게 만들었다.
1분 요약 — 이 글에서 얻어갈 것
AI 공동 개발자와 함께 만들고 있는 앱이 사용자에게 “로그인 링크를 보냈습니다!”라고 말했다 — 그 뒤에는 이메일 서비스가 아예 없었다. 버그를 하나씩 고치는 대신, 우리는 제품이 내건 모든 약속(80개)을 적어두고 그중 20개 뒤에 기계 검증을 걸었다. 정직한 스코어보드는 하루 만에 초록불 6개에서 15개로 늘었다 — 그리고 감사 체계는 아무나 들어올 수 있게 열려 있던 관리자 뒷문을 잡아냈다. 이 모든 게 작동한 건 단 하나의 규칙 덕분이었다: 증거 없이는 통과도 없다.
노인을 위한 자서전 앱, 거짓말을 한 성공 토스트, 그리고 두더지 잡기 게임을 끝낸 주장 장부에 관한 빌드인퍼블릭 이야기. 약 8분.
거짓말을 한 토스트
내가 만들고 있는 것은 新遗产传记 (뉴 레거시 바이오그래피) — 나이 든 어르신 곁에서 AI 인터뷰어가 함께 앉아, 손주가 미처 물어볼 생각도 못했던 질문들을 던지고, 그 대답을 책의 한 장(章)으로 만들어주는 앱이다. 이런 제품에서는 버그 하나가 그저 '불편함'으로 끝나지 않는다. 할머니가 자라온 강가 이야기를 소프트웨어가 잃어버린다면, 그건 다시 다운로드할 수 없는 것을 잃는 셈이다.
이 프로젝트의 공동 개발자는 AI 에이전트다. 속도만큼은 빠르다. 유닛 테스트는 전부 초록불이었다 — 135개 전부. TypeScript strict 모드도 깨끗했고, Lint 오류도 0건이었다. 그런데 내가 평범한 사용자 역할을 맡아 이메일 링크로 로그인을 시도해봤다. 화면에는 이렇게 떴다: “登录链接已发送!” — your login link has been sent, check your inbox. (번역: 로그인 링크가 발송되었습니다! 받은편지함을 확인하세요.)
이메일은 끝내 오지 않았다. 올 수가 없었다. 프로덕션 어디에도 이메일 서비스가 설정되어 있지 않았기 때문이다. 코드에는 아무도 들여다보지 않는 서버 로그에 이메일을 찍어내는 '개발용 폴백'이 있었고, 그러고는 사용자에게 성공했다고 알렸다. 우리가 믿었던 모든 게이트는 초록불이었지만, 제품은 정작 가장 중요한 그 한 사람에게 거짓말을 하고 있었다.
더 놀라운 대목은 따로 있었다. 이 제품의 핵심 기능인 '가족 초대하기' — 친척들이 생일 기념 앨범에 사진과 음성 메모를 보태는 기능 — 가 생성하는 초대 링크는 다음으로 시작했다: http://localhost:3000. 오직 내 책상 위에만 존재하는 컴퓨터를 가리키는 링크였다. 다른 도시에 사는 이모에게 그걸 공유해봐야 아무 일도 일어나지 않는다. 게다가 회원가입 페이지의 유일한 경로도 그 똑같은 유령 이메일이었다 — 즉 한동안은, 지구상 그 누구도 이 제품에 새로 가입할 수 없었다, 그런데도 모든 테스트는 계속 초록불이었다.
초록불 테스트가 우리를 구하지 못한 이유
Edsger Dijkstra는 1972년 튜링상 수상 강연에서 조용히 이 말을 남겼다: "program testing can be a very effective way to show the presence of bugs, but is hopelessly inadequate for showing their absence" (번역: 프로그램 테스트는 버그의 존재를 보여주는 데는 매우 효과적일 수 있지만, 버그의 부재를 보여주는 데는 절망적으로 부적합하다) [1]. 54년이 지난 지금, AI 보조 개발은 그의 경고를 무디게 만들기는커녕 오히려 더 날카롭게 만들었다 — 생성형 도구가 완성된 것처럼 보이는 코드를 만드는 데 지나치게 능하기 때문이다. 유닛 테스트는 함수를 테스트한다. 아무도 약속.
을 테스트하고 있지 않았다. 오랫동안 ML 제품을 만들어온 Hamel Husain은 같은 진단을 현대적인 언어로 이렇게 표현한다: "I've found that unsuccessful products almost always share a common root cause: a failure to create robust evaluation systems" (번역: 실패한 제품들은 거의 항상 공통된 근본 원인을 공유한다는 것을 발견했다: 견고한 평가 시스템을 구축하지 못했다는 것이다) [2]. 정작 훔쳐올 만한 가치가 있는 건 그다음 추론이다: 평가 시스템이 없으면, 실패 하나를 고쳐봐야 다른 실패가 그저 표면으로 드러날 뿐이라는 것 — 그는 이를 "a game of whack-a-mole" (번역: 두더지 잡기 게임)라 불렀다[2]. 딱 내가 겪은 그 한 주였다. 이메일 거짓말을 고치면 localhost 링크가 발견되고, 그 링크를 고치면 음성 입력에 배포된 적 없는 모델 파일이 필요하다는 게 드러났다. 두더지를 잡을 때마다 새로운 두더지가 튀어나왔다. 아무도 점수를 매기고 있지 않았기 때문이다.
멘탈 모델: 주장 장부
전체 아이디어는 이렇다. 이 글 없이도 내일 그대로 재현할 수 있을 만큼 단순하다. 당신의 제품이 내거는 모든 약속 — 버튼 하나하나, 내비게이션 항목 하나하나, 마케팅 문구 한 줄 한 줄 — 은 주장. 머신 체크가 없는 주장은 소문일 뿐이다. 그러니:
- 주장을 데이터로 적어라. 머릿속이나 문서가 아니라, 코드가 읽을 수 있는 파일 안에. 우리 파일에는 “亲友无需注册可上传照片”(등록 없이도 가족·친구가 사진을 올릴 수 있다)와 “新用户可以完成注册”(신규 사용자가 실제로 가입을 완료할 수 있다) 같은 항목이 들어 있다.
- 각 주장마다, 사용자가 하듯이 그것을 직접 실행해보는 체크를 하나씩 만들어라. 실제 브라우저가 프로덕션 사이트의 실제 폼을 채운다. 쿠키 없는 요청이 이모가 링크를 열어볼 법한 방식 그대로 초대 링크를 연다.
- 두 단계가 아니라 세 단계로 정직하게 점수를 매겨라: PASS, FAIL, 그리고 NOT MEASURED. 이 세 번째 등급이 핵심을 지탱한다 — 확인할 수 없었던 주장은 제외될 뿐, 슬그머니 초록색으로 세어지지 않는다.
- 그것을 게이트로 걸어라. 게이트가 걸린 주장이 실패하면 실행 자체가 화를 내며 멈춘다. 가짜 ✅보다 정직한 ❌이 낫다.
하루의 정직함이 만들어낸 결과
프로덕션을 상대로 한 첫 전체 실행에서 점수는 PASS 6, FAIL 11, NOT MEASURED 3이었다 — 머신 체크된 주장 20개를 대상으로. 앱이 대체로 쓰레기였기 때문이 아니라, 몇 달 동안 아무도 이 앱에 뭔가를 증명해보라고 요구한 적이 없었기 때문이다. 그날이 끝날 무렵에는: PASS 15개로 늘었고, 남은 FAIL들은 모호한 불길한 느낌 대신 이름 붙은, 범위가 명확한 해결 과제를 갖게 되었다.
입장료 값을 하는 세 가지 발견:
- 셀프 가입이 막혀 있던 구멍. 등록 페이지는 이메일 링크 방식뿐이었는데, 프로덕션에는 이메일이 없었다. 체크는 빨간 글씨로 FAIL을 띄웠고, 우리는 비밀번호 경로를 추가했다. 이제 같은 체크가 로봇 사용자가 실제 서비스에서 가입을 완료하는지 영원히 지켜본다.
- 실패 시 열리는 관리자 문 이 사례가 특히 마음에 드는 이유는, 감사 체계가 바로 the regime's own maker shipped the same day(번역: 이 체계를 만든 사람 자신이 같은 날 만들어낸) 버그를 잡아냈기 때문이다. 우리는 관리자 콘솔을 추가했는데, 인가 가드의 로직은 이러했다: 사용자에게 역할이 있고 그 역할이 허용되지 않은 경우에만 거부한다는 것이었다. 괜찮아 보인다 — 로그인 토큰에 역할 필드 자체가 아예 없이 도착해서, 검사를 건너뛰고 유유히 들어오기 전까지는. 우리가 갓 만든 테스트 계정은 관리자 API로부터 HTTP 200을 받았다. 그 문지기의 규칙은 말 그대로 이랬던 셈이다: 신분증이 없으면, 문제없는 사람으로 간주한다. 검증 단계가 몇 분 만에 이를 잡아냈고, 이제 인가는 닫힘(closed) 쪽으로 실패하도록 바뀌었으며, 여기에 대한 영구 체크도 마련되었다.
- 열 가지 증상 뒤에 숨은 시스템적 버그 감사는 계속 같은 패턴을 발견했다: 실행된 적 없는 백그라운드 사진 처리, 사라진 내보내기 작업, 먹통이 된 텍스트 음성 변환 경로
localhost:8888. 근본 원인은 한 문장으로 정리됐다: 코드는 계속 살아있는 서버를 전제로 작성됐지만, 실제로는 응답을 마치는 순간 얼어붙는 서버리스 함수 위에서 돌아가고 있었다. 우리 코드는 계속해서, 작별 인사를 하는 즉시 취소되어 버리는 미래에 작업을 예약하고 있었던 셈이다. 열 개의 미스터리 버그가 하나의 아키텍처 질문으로 수렴했고, 이제 새 기능마다 이 질문에 답해야 한다.
패턴과 안티패턴
통했던 패턴들:
- 데이터로서의 주장, 코드로서의 체크. 명세는 테스트에서 벗어날 수 없다, 왜냐하면 명세 자체가 곧 테스트의 입력이기 때문이다.
- 사용자의 눈높이에서 테스트하기. 프로덕션 URL 위에서 실제 브라우저로, 오직 실시간 데이터만이 답할 수 있는 질문을 던진다. 픽스처는 코드가 돌아간다는 것을 증명하고, 프로덕션은 약속이 지켜졌다는 것을 증명한다.
- 요란하게 실패하라, 절대 속이지 말라. 이제 이메일 서비스는 유쾌한 거짓말 대신 "이메일이 설정되어 있지 않습니다 — 비밀번호 로그인을 사용하세요"라는 정직한 메시지를 띄운다. 사용자는 빠진 기능은 용서해도, 가짜 기능은 용서하지 않는다.
- 두 등급이 아니라 세 등급. NOT MEASURED는 초록불을 조작하고 싶은 유혹을 없애주는 압력 배출 밸브다.
우리가 대가를 치른 안티패턴들:
- 결과물이 아니라 라벨을 믿는 것. "테스트 135개 통과"는 라벨이다. 낯선 사람이 가입을 완료하는 것이 결과물이다.
- 조용한 폴백. 그럴듯한 성공으로 대체해버리는
catch블록은 지연 타이머가 달린 거짓말 생성기다. - "안녕"으로 하는 인수 테스트. 통조림 같은 답변도 "안녕"은 통과한다. AI 인터뷰어에게는 실제 데이터베이스만 알 수 있는 것을 물어봐야 한다.
이 메커니즘의 정체는 이렇다: 생성형 도구는 그럴듯해 보이는 완성 — 완성된 것처럼 보이는 코드 — 를 최적화한다. 그 목표 어디에도 이메일이 실제로 존재해야 한다는 조건은 없다. 그러니 가짜-완료를 향한 압력은 도덕의 문제가 아니라 구조의 문제이고, 그 대응책 역시 구조적이어야 한다.
첫 번째 원칙은 한 문장으로 충분하다: 증거 없이는 통과도 없다.
월요일에 바로 훔쳐 써먹기
시간 제한을 두고 구체적으로 — 작업 세션 한 번이면 된다:
- (30분) 제품의 랜딩 페이지와 내비게이션을 열어보고, 그것이 하는 약속 20개를 있는 그대로 JSON 파일에 적는다.
- (2~3시간) 각 약속마다, 사용자가 하듯이 그것을 실행해보는 가장 단순무식한 체크를 작성한다 — 헤드리스 브라우저나 curl로, 스테이징이 아닌 프로덕션에, 스테이징이 아니라 프로덕션을 대상으로 검사한다. 각 주장에는 다음 표시를 단다
gate: true/false. - (10분) 실행한다. 팀이 볼 수 있는 곳에 정직한 표를 공개한다 — PASS, FAIL, NOT MEASURED.
- 성공은 숫자 하나로 측정한다: 게이트가 걸린 FAIL이 0건이고, 이 실행이 CI에 연결되어 조용히 썩어갈 수 없는 상태. 확신하던 릴리스를 막아서는 순간, 제대로 작동한다는 걸 알게 될 것이다.
우리 것은 레포 안에 있으며, 이름은eval/claims.json + eval/run.mjs — 주장 20개, 명령어 하나, 정직한 출력이다. 이 패턴은 내가 상시 규율로 지키는 OEC 루프(Observe → Evaluate → Control)의 한 적용 사례다: 관찰 가능성이 평가에 앞서고, 통제 훅이 없는 평가는 그저 스코어보드일 뿐이다.
소프트웨어에 관한 이야기가 아닌 부분
이 일이 일어난 앱은 실제로 운영 중이다: AI 인터뷰어가 표준중국어로 대화를 나누며 기억을 따라 부엌까지 들어가서 — 지난주 진행한 테스트 인터뷰 하나는 대추와 말린 용안 냄새가 나는 기억이었다 — 그것을 한 챕터로 써낸다. 여기에 실제 데이터를 바탕으로 "내 책은 얼마나 진행됐나요?"라는 질문에 답하는 동반 에이전트까지 갖췄다. 이번 주 이 앱은 어떤 기능보다 드문 것을 하나 얻었다: 자기 자신에 대해 진실을 말하는 습관이다. 언젠가는 기록해야지 하고 미뤄둔 부모님의 이야기가 있다면, 여기 있다 — 그리고 이 앱이 지금 할 수 있는 것과 아직 할 수 없는 것을 보여주는 스코어보드는 공개되어 있다. 제품을 판다면 바로 이런 방식으로 팔아야 한다고, 나는 생각한다.
참고 문헌
- Dijkstra, E. W. (1972). The Humble Programmer (ACM 튜링상 강연), EWD340: "program testing can be a very effective way to show the presence of bugs, but is hopelessly inadequate for showing their absence." (번역: 프로그램 테스트는 버그의 존재를 보여주는 데는 매우 효과적일 수 있지만, 버그의 부재를 보여주기에는 절망적일 만큼 부적합하다.) cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340.html
- Husain, H. (2024). Your AI Product Needs Evals. "I've found that unsuccessful products almost always share a common root cause: a failure to create robust evaluation systems"(번역: 나는 실패하는 제품들이 거의 항상 하나의 공통된 근본 원인을 공유한다는 것을 발견했다: 견고한 평가 시스템을 만들지 못한 실패다); 증상에 대해서는: "Addressing one failure mode led to the emergence of others, resembling a game of whack-a-mole."(번역: 하나의 실패 양상을 해결하면 다른 것들이 나타났고, 마치 두더지 잡기 게임과 같았다.) hamel.dev/blog/posts/evals
AI-Native 시리즈 더 보기
AI-Native 시리즈의 일부다. 이 글에 등장하는 모든 내용은 공개된 평가 원장에서 그대로 재현 가능하다 — 실패 사례까지 포함해서. 증거 없이는 통과도 없다. Publish 버튼을 누르는 건 결국 당신이다.