0개 실행도 0 실패다 — 바이브 코딩 MVP 와 하네스의 완료기준
“다 됐어요” 는 두 사람 입에서 나올 때 같은 뜻이 아니다.
한쪽은 화면이 떴다는 뜻이고, 다른 쪽은 내가 없어도 같은 결론이 나온다는 뜻이다. 이 글은 그 두 완료기준이 각각 무엇이고, 어디서 갈라지고, 전환을 선언하지 않으면 무슨 일이 나는지에 대한 것이다. 사례는 추상적인 게 아니라 내 클러스터와 내 리포에서 실제로 났던 오판들이고, 이론 쪽은 전부 1차 출처를 달았다.
1. 바이브 코딩의 완료기준은 원저자가 이미 못박아뒀다
“바이브 코딩(vibe coding)” 이라는 말은 Andrej Karpathy 가 2025년 2월 2일 X 게시물에서 만들었다.1 이 글에서 중요한 건 그가 유행어를 만들었다는 사실이 아니라, 완료기준과 적용범위를 같이 적어놨다는 것이다.
그는 자신의 검증 절차를 이렇게 적었다 — "Accept All" always, 디프는 더 이상 읽지
않는다고. 에러가 나면 코멘트 없이 복사해 붙이고, 코드는 이미 자기 이해를 넘어 자란다고.
그리고 완료 판정은 마지막 문장에 있다: it mostly works.
그러니까 바이브 코딩의 완료기준은 이렇게 쓸 수 있다.
\[\text{완료}_{\text{MVP}} \;=\; \text{“내가 보기에 돌아간다”}\]여기서 흔히 놓치는 대목은, 원저자가 적용범위를 직접 한정했다는 점이다. 원문은 이걸
throwaway weekend projects — 버릴 주말 프로젝트에는 나쁘지 않다고 표현한다.
이건 결함이 아니다. 폐기를 전제로 설계된 기준이다. MVP 의 목적은 정확성 보장이 아니라 불확실성 해소다. “이 방향이 말이 되나?” 를 하루 만에 알아내는 게 목적이면 6단 게이트는 배움을 방해할 뿐이다. 이 단계에 하네스를 씌우면 MVP 가 죽는다.
문제는 그 물건을 안 버릴 때 시작된다. 그리고 대부분의 사고는 버리지 않기로 결정한 순간이 아니라, 버리지 않기로 결정해놓고 완료기준만 주말 프로젝트에 남겨둔 채로 지나갈 때 난다.
2. 그 기준이 언제 거짓말을 시작하는가
“내가 보기에 돌아간다” 가 무너지는 방식은 하나뿐이다.
내가 보는 지점과, 물건이 실제로 쓰이는 지점이 다르다.
내 클러스터·리포에서 실제로 났던 것들을 날짜와 함께 놓는다. 전부 초록불이 떠 있는 상태에서 틀려 있었다.
| 초록불 | 실제 | 무엇이 어긋났나 |
|---|---|---|
BUILD SUCCESSFUL |
통합테스트 클래스 전체가 실행 안 됨 | Docker 없으면 @EnabledIf 가 조용히 스킵 |
ArgoCD Synced / Healthy |
파드 0개 (2026-08-12, 4개 네임스페이스) | 선언 일치 ≠ 트래픽 수신 |
사이드카 POST /synthesize 200 OK |
호출자는 502 (2026-08-14) | 디코더가 응답을 다 받은 뒤 터짐 |
serviceMonitor.enabled=true |
75일간 지표 0줄 (2026-08-05 발견) | 선택 홉이 끊겨 있었음 |
파드 Ready |
port-forward 는 connection refused |
루프백이 아니라 파드 IP 에만 바인딩 |
GitHub Pages built |
글이 안 뜸 | 미래 date 는 조용히 제외됨 |
| 검증기 테스트 초록 | 검증기가 참가자와 무관하게 100% 실패 판정 | 기대값이 손으로 적힌 상수였음 |
몇 개만 풀어 쓴다.
빌드. Testcontainers 통합테스트는 보통 @EnabledIf("isDockerAvailable") 로 걸려 있다.
로컬에 Docker 가 안 떠 있으면 클래스 전체가 스킵되고, 실패도 경고도 없이 BUILD SUCCESSFUL
이 뜬다. 초록불의 의미는 “통과” 가 아니라 “실패가 없음” 이다. 그리고 0개 실행도 0 실패다.
더 나쁜 변종은 @EnabledIfEnvironmentVariable 류다 — CI 에 그 키가 없으면 그 검사는
로컬 한정이 아니라 영구히 안 돈다.
관측. serviceMonitor.enabled=true 는 지표가 수집된다는 뜻이 아니다. 독립된 두 홉이
각각 성립해야 하고 둘 다 조용히 실패한다 — ① ServiceMonitor.spec.selector 가 고르는 건
바로 아래 있는 Service.spec.selector 가 아니라 Service 의 metadata.labels 다(단어가
같아서 눈으로 구분이 안 된다). ② 대상을 잡아도 endpoint 가 인증을 요구하면 up=0 이다.
2026-08-05 에 ①번 때문에 75일간 한 줄도 수집된 적이 없었다. 대시보드에는 그냥 빈 그래프로
보였다. 지표 수집이 멈췄다는 사실 자체는 지표로 보이지 않는다.
홉이 늘수록 관측 확률은 곱으로 줄어드는데, 각 항의 실패가 예외를 던지지 않는다. 그래서 전체가 0 이어도 화면은 정상으로 보인다.
응답. 업스트림 로그가 전부 200 인데 호출자만 5xx 면 네트워크·타임아웃을 뒤지기 전에
클라이언트 디코더 한도를 봐야 한다. Spring WebClient 의 기본 인메모리 버퍼는
262,144 바이트다. 넘으면 DataBufferLimitException 이 응답을 다 받은 뒤에 터지므로
업스트림 관점에서는 완벽한 성공이고 어느 로그에도 증상이 안 남는다. 이 실패는 절벽이라
간헐적 버그로 위장한다 — 성공해 있던 캐시 행 2개의 base64 길이가 각각 207,654 와 260,902
였다. 후자는 한도까지 여유가 1,242자(0.47%) 였다. 짧은 문장은 멀쩡히 재생되고 긴 것만
죽으니 “가끔 안 된다” 로 보인다.
3. 하네스가 실제로 하는 일 — 판정을 주장에서 관측으로 옮긴다
하네스가 뭘 하는지 보려면, 완료기준을 코드로 쓸 수밖에 없었던 사례를 보는 게 빠르다. SWE-bench 가 그렇다.2 “AI 가 이 GitHub 이슈를 해결했는가” 를 사람 판단 없이 정해야 했기 때문에, “해결” 의 정의가 실행 가능한 코드로 남아 있다.
정의는 두 항이다.
- F2P (Fail-to-Pass) — 패치 전에 실패하던 테스트가 이제 통과하는가. 고쳤는가.
- P2P (Pass-to-Pass) — 통과하던 테스트가 여전히 통과하는가. 안 부쉈는가.
한 항만으로는 게임할 수 있기 때문에 둘 다 필요하다. F2P 만 보면 나머지를 부수고 통과할 수 있고, P2P 만 보면 아무것도 안 고쳐도 만점이다. 이 정도는 상식이다.
진짜 볼 것은 그 다음이다 — 이 하네스가 어떤 사고를 막으려고 코드를 추가로 썼는가.
swebench/harness/grading.py 에는 세 개의 방어 장치가 있고, 셋 다 같은 가족의 결함을
막는다.3
(가) 스킵을 해결로 세지 않기. test_failed 안에서 SKIPPED 를 실패 쪽에 넣는 줄에
주석이 붙어 있다 — 이게 없으면 모든 F2P 를 스킵시키는 패치 가 성공 목록에도 실패 목록에도
안 들어가서 RESOLVED_FULL 로 채점된다는 것이다. 바로 앞 절의 내 @EnabledIf 사고와
정확히 같은 결함 클래스다. 세계에서 제일 많이 쓰이는 코딩 에이전트 하네스도 이걸 막으려고
코드를 따로 썼다.
(나) “안 돈 것” 과 “돌았는데 실패가 없는 것” 을 구분하기. 파일 상단에 SUITE_RAN 이라는
정규식이 있는데, 그 존재 이유가 주석에 그대로 적혀 있다: "ran with no failures" 를
"never ran" 과 구분하기 위해서다. 예시로 karma 가 Executed 0 of 0 을 찍는 경우를 든다.
파싱 결과가 비었고 스위트가 돌았다는 증거도 없으면 판정은 이렇게 난다 —
the run is invalid, not a pass. 안 돈 것은 통과가 아니다.
이건 내가 한국어로 적어둔 규칙과 같은 문장이다. 판별 신호는 ignored=0 이고, 확인할 곳은
초록 체크마크가 아니라 build/test-results/test/*.xml 의 tests= / failures= / ignored= 다.
(다) 채점당하는 쪽이 쓴 증거를 그대로 믿지 않기. 패치가 conftest.py 훅으로 자기
PASSED 줄을 찍을 수 있으므로, 로그를 테스트 명령의 종료 코드와 대조한다. 종료 코드가
0이 아닌데 로그에 실패가 하나도 없으면 판정은 the log is not describing the run that
actually happened — 그 로그는 실제로 일어난 실행을 서술하고 있지 않다.
여기에 덤이 하나 더 있다. compute_fail_to_pass 는 대상이 0개면 1 을 돌려준다. 공집합은
만점이다. 완료기준을 코드로 쓰면 이런 공허참(vacuous truth)이 드러난다. 산문으로 쓰면
안 드러난다. 규약을 JSON 이나 문서 안의 문장으로만 적어두면 그걸 읽는 코드가 없어서
아무것도 깨지지 않는다.
정리하면 하네스가 하는 일은 이거다.
\[\text{완료}_{\text{harness}} \;=\; \text{“제3자가 재현할 수 있는 관측 증거가 있다”}\]주어가 “나” 에서 “제3자” 로 바뀐 것이 전부이고, 그게 전부를 바꾼다.
4. 이건 새 발명이 아니다
Scrum Guide (2020). “Definition of Done” 은 증분이 제품에 요구되는 품질 기준을 만족한 상태에 대한 공식적 서술 이라고 정의된다. 그리고 강한 문장이 뒤따른다 — DoD 를 만족하지 못한 항목은 릴리스는커녕 스프린트 리뷰에서 발표조차 할 수 없고 백로그로 돌아간다.4 완료기준을 팀이 공유하는 명시적 형태로 못박는 관행 자체는 이미 표준 문서에 들어가 있다는 뜻이다.
Dijkstra (1969). 1969년 10월 로마에서 열린 NATO 소프트웨어공학 기법 회의 보고서 16쪽에
가장 이른 형태가 남아 있다 — 테스트는 버그의 존재를 보일 수 있지만 부재는 보이지
못한다.5 같은 명제가 EWD249 「Notes on Structured Programming」(1969년 8월)
3절 끝의 따름정리로도 적혀 있다.6
왜 그런지에 대한 그의 계산이 EWD303 에 있다.7 27비트 워드의 고정소수점 곱셈기를
전수 검증하려면 $2^{54}$ 가지를 다 돌려야 하고, 초당 $2^{14}$ 회로 돌려도
곱셈기 하나를 한 번씩 다 시험하는 데 3만 년이 걸린다. 완료기준이라는 건 전수 검증이 불가능하다는 전제 위에서 설계하는 물건이라는 뜻이다. 그래서 “다 테스트했다” 는 애초에 불가능한 주장이고, 가능한 주장은 “무엇을 어떤 조건에서 관측했다” 뿐이다.
5. 그럼 그 초록불이 무언가를 지키는지는 어떻게 아나
테스트가 있다는 것과 그 테스트가 결함을 잡는다는 것은 다르다. 이 구분을 처음 형식화한 게 DeMillo·Lipton·Sayward 의 1978년 논문이다.8 프로그램에 인위적 변이를 심고 테스트가 그걸 죽이는지를 본다.
\[\text{mutation score} \;=\; \frac{\text{killed}}{\text{total} - \text{equivalent}}\]커버리지는 코드가 실행됐는지를 재고, 돌연변이는 결함이 검출되는지를 잰다. 앞 절의 디코더 한도 사고에서 내가 손으로 한 게 정확히 이거였다. 스텁 응답이 수백 바이트면 회귀 테스트는 한도를 건드릴 일이 없어서 영원히 초록이다. 그래서 회귀 테스트를 한도를 넘는 크기로 만들고, 한도만 되돌리면 실패하는 대조군을 같이 뒀다. 대조군이 없으면 그 초록은 아무것도 지키지 않는다.
그리고 하네스도 틀린다. 2026-08-22 에 lemuel-xr PR #102 에서 겪은 일이다. job.yml 에
주석 두 줄을 넣었더니 어떤 키가 146행에서 148행으로 밀렸고, 문서가 그 줄 번호를 본문에
인용하고 있었다. 도미노는 세 단계였다 — 검사 rc 0→1 → 실측 칸 불일치 → 합계 선언 불일치.
그 결과 돌연변이 게이트가 그 축에 변이를 못 심어서 검출이 13/16 으로 줄고 게이트가
떨어졌다. 여기서 배운 것:
- 돌연변이 게이트가 실패하면 검출 수보다 기준선 FAIL 집합을 먼저 본다. 기준선이 이미 빨간 축은 변이를 못 심으므로, “게이트 통과” 와 “게이트가 그 축을 못 봄” 이 겉으로 구별되지 않는다.
- 문서가 코드의 줄 번호를 인용하면 무관한 편집마다 거짓이 된다. 자구 앵커로 인용하면 구조적으로 안 밀린다.
6. 하네스의 비용 — 정직하게
완료기준을 코드로 옮기면 문제가 사라지는 게 아니라 옮겨간다. 옮겨간 자리를 세 개 적는다.
(1) 검증기를 누가 검증하나. 오늘 아침 inter-asat 의 JND 계단법을 문헌과 대조한 글 에서 확인한 게 이거였다. 세션 데이터를 인증하는 재생 검증기가 프론트가 보낸 퍼센트를 Hz 로 재생하고 있어서, 참가자가 누구든 상대오차가 $|100/f_{\text{ref}} - 1|$ 이라는 상수로 떨어졌다. 참가자의 실제 역치는 식에서 완전히 소거된다. 그 검증기의 단위 테스트는 초록이었다 — 기대값이 손으로 적힌 상수였기 때문이다. 하네스는 자기 자신을 검증하지 않는다.
(2) Goodhart. “측정이 목표가 되면 좋은 측정이기를 그친다” 는 문장은 Marilyn Strathern 이 1997년 논문에서 쓴 형태가 가장 널리 인용되는 판본이고, 그 논문 안에서 그녀는 이 이름 붙이기를 Hoskin 에게 돌리고 있다(Goodhart 의 원래 관찰은 통화 관리 수단에 대한 것이다).9 게이트를 통과하려고 실측 칸을 고치는 순간 게이트는 죽는다. 그래서 나는 이걸 규칙으로 못박아 뒀다 — 빨강을 초록으로 옮겨 적지 않는다. 틀린 것은 인용이지 표가 아니다.
(3) 속도. MVP 단계에 6단 게이트를 씌우면 배울 게 없다. 하네스의 값은 버리지 않기로 결정한 뒤에 생긴다. 순서를 뒤집으면 둘 다 잃는다.
7. 내가 쓰는 완료기준 계층
각 층이 어떤 방식으로 거짓말하는지를 같이 적어야 쓸모가 있다.
| 층 | 신호 | 이 층이 거짓말하는 방식 | 실제로 봐야 할 것 |
|---|---|---|---|
| L0 주장 | “됐어요” | 전부 | — |
| L1 빌드 | BUILD SUCCESSFUL |
0개 실행도 0 실패 | ignored=0, tests= 실수치 |
| L2 테스트 | 초록 | 대조군 없는 초록은 아무것도 안 지킴 | 돌연변이 검출 수 + 기준선 FAIL 집합 |
| L3 배포 | Ready / Synced·Healthy |
선언 일치 ≠ 트래픽 수신 | 사용 지점에서의 실제 응답 |
| L4 관측 | 대시보드가 있음 | 빈 그래프와 무수집이 같아 보임 | up{job=...} == 1, 마지막 비영(非零) 성공 시각 |
| L5 재현 | “제가 확인했습니다” | 나만 재현 | 남이 같은 명령으로 같은 결론에 도달 |
그리고 이 계층을 실제로 지키게 만드는 규칙 몇 줄. 전부 틀려본 뒤에 생긴 것들이다.
- 관측 실패를 선언했으면 그 뒤로 사실 문장을 쓰지 않는다. 2026-08-13 아침 크론 브리핑이
curl rc=7로 “관측 불능” 을 선언하고도 브리핑을 끝까지 썼다. 그래서 실제로는 살아서 수집 중이던 파이프라인을 “정리 후보” 로 올렸다. 사고의 본질은 못 본 게 아니라 못 봤다고 선언하고도 계속 쓴 것이다. 지금은 프로브가 첫 줄에PROBE_STATUS를 찍고 실패면 사실 0줄로exit 1한다. - 도달 실패를 장애로 단정하지 않는다. apiserver 는 인증 없이 찌르면 401 을 준다.
401 은 살아 있다는 증거다. 연결 실패(
rc=7)와 혼동하면 멀쩡한 클러스터가 죽은 걸로 보고된다. - 고쳤는지는 성공/실패가 아니라 “실패의 종류가 바뀌었는지” 로 판정한다. 버퍼 한도를
고쳐 배포한 뒤에도 첫 요청은 여전히 502 였는데, 예외가
DataBufferLimitException에서TimeoutException 30000ms로 바뀌어 있었다. 수정은 먹었고 그 다음 벽이 드러난 것이다. 이걸 구분 못 하면 맞는 수정을 되돌리게 된다. - 불변식은 키가 필요한 검사에 두지 않는다. 네트워크·시크릿이 필요한 검사는 CI 에서 상시 스킵될 수 있다. 설정↔선언 대조처럼 네트워크 없이 도는 형태로 바꿔야 실제로 지켜진다.
이 글의 한계
- SWE-bench
grading.py는 오늘main브랜치의 소스를 읽은 것이고 커밋을 고정하지 않았다. 인용한 주석은 앞으로 바뀔 수 있다. - 내 사례들은 각각 표기한 날짜의 실측이며 현재 상태에 대한 주장이 아니다.
- Karpathy 원문은 X 게시물이라 원본과 아카이브를 함께 달았다.
- 하네스 도입이 결함률을 얼마나 낮추는지에 대한 정량 주장은 하지 않았다. 내 사례는 단일 조직의 사후 관찰이고 대조군이 없다.
결론
바이브 코딩 MVP 의 완료기준은 “내가 만족했는가” 이고, 하네스의 완료기준은 “내가 없어도 같은 결론이 나오는가” 이다.
전자는 폐기를 전제로 할 때 완전히 정당하다 — 원저자부터 그렇게 한정해서 썼다. 후자는 폐기하지 않기로 한 순간부터 필수다. 대부분의 사고는 둘 중 하나가 나빠서가 아니라, 그 전환을 선언하지 않고 지나갈 때 난다. 물건은 프로덕션에 가 있는데 완료기준만 주말 프로젝트에 남아 있는 상태.
그리고 그 전환의 첫 번째 문장은 언제나 같다.
초록불은 “통과” 가 아니라 “실패가 없음” 이다. 0개 실행도 0 실패다.
References
-
Andrej Karpathy, X 게시물, 2025-02-02. 원본: https://x.com/karpathy/status/1886192184808149383 · 아카이브: https://archive.is/yNSTA (2025-02-03 아카이브) ↩
-
Jimenez, C. E., Yang, J., Wettig, A., Yao, S., Pei, K., Press, O., & Narasimhan, K. (2024). SWE-bench: Can Language Models Resolve Real-world Github Issues? ICLR 2024. https://openreview.net/forum?id=VTF8yNQM66 · 과제 구성과 Fail-to-Pass 설명: https://www.swebench.com/original.html ↩
-
SWE-bench 평가 하네스 소스,
swebench/harness/grading.py(main브랜치, 2026-08-24 열람). https://github.com/SWE-bench/SWE-bench/blob/main/swebench/harness/grading.py ↩ -
Ken Schwaber & Jeff Sutherland, The Scrum Guide (2020년 11월판), “Commitment: Definition of Done”. https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-US.pdf ↩
-
J. N. Buxton & B. Randell (eds.), Software Engineering Techniques: Report on a conference sponsored by the NATO Science Committee, Rome, Italy, 1969-10-27~31 (발간 1970-04), p.16. ↩
-
E. W. Dijkstra, Notes on Structured Programming (EWD249), 1969-08, §3 “On the Reliability of Mechanisms” 말미의 따름정리. https://pure.tue.nl/ws/files/2408738/252825.pdf ↩
-
E. W. Dijkstra, On the reliability of programs (EWD303). E.W. Dijkstra Archive, University of Texas at Austin. https://www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD303.html ↩
-
R. A. DeMillo, R. J. Lipton, F. G. Sayward. “Hints on Test Data Selection: Help for the Practicing Programmer.” IEEE Computer 11(4):34–41, 1978-04. doi:10.1109/C-M.1978.218136 ↩
-
Marilyn Strathern. “‘Improving ratings’: audit in the British University system.” European Review 5(3):305–321, 1997. https://www.cambridge.org/core/journals/european-review/article/abs/improving-ratings-audit-in-the-british-university-system/FC2EE640C0C44E3DB87C29FB666E9AAB — 해당 문장과 Hoskin 의 명명 언급은 본문 308쪽. ↩