체크표시는 결코 증거가 아니었다
요약
테스트 통과(초록색 체크표시)가 반드시 코드의 무결성을 증명하는 것은 아니라는 점을 지적합니다. 플래키 테스트의 재시도 방식이 버그를 은폐하는 문제와 불충분한 단언(assertion)의 위험성을 다룹니다.
핵심 포인트
- CI 재시도 설정은 버그를 해결하는 것이 아니라 통계적 확률만 바꿀 뿐입니다.
- 플래키 테스트의 주요 원인은 무작위성이 아닌 동시성 및 비동기 대기 등 결정론적 버그입니다.
- 메시지 없는 단언은 실패 원인을 파악하기 어렵게 만듭니다.
- 테스트 통과는 검증이 아닌 하나의 주장일 뿐임을 인지해야 합니다.
체크표시는 결거 증거가 아니었다
저는 생업으로 테스트 스위트 (test suites)를 작성하지 않습니다. 제 업무는 그것들을 채점하는 것이며, 애초에 코드를 작성하고 판단하는 AI 모델들을 채점하는 것입니다. 제가 따르는 루브릭 (rubric)은 동점을 허용하지 않습니다. 모든 판결에는 명시된 파일, 줄 번호, 구체적인 전후 상태가 필요합니다. "이게 더 깔끔해 보인다"는 이유는 근거가 될 수 없으며, 이는 거절 사유입니다.
그 자리에 오래 앉아 있다 보면 업계의 나머지 사람들이 겉으로 말하기를 꺼리는 사실 하나를 배우게 됩니다. 그것은 바로 초록색 체크표시가 결코 무엇인가를 증명한 적이 없었다는 사실입니다. 그것은 언제나 하나의 주장 (claim)이었으며, 저는 그 주장이 완전히 다른 세 가지 환경에서 동일한 방식으로 실패하는 것을 목격해 왔습니다.
첫 번째 실패: 버그를 숨기는 재시도 (retry)
플래키 테스트 (Flaky tests, 간헐적 실패 테스트)는 이것이 가장 눈에 띄게 나타나는 형태입니다. Google의 자체 엔지니어링 팀은 2016년에 자신들의 테스트 중 약 16%가 어떤 식으로든 플래키 (flaky)한 동작을 보였으며, 전체 테스트 실행 중 특정 날짜에 약 1.5%가 플래키하게 돌아왔다고 보고했습니다. 이는 엔지니어들이 작성한 테스트 7개 중 약 1개가 실제 코드 변경과 무관한 이유로 가끔 실패한다는 것을 의미합니다.
표준적인 해결책인 CI 설정에서의 재시도 횟수 (retry count) 지정은 근본적인 버그를 제거하지 않습니다. 그저 확률을 바꿀 뿐입니다. 실제 실패율이 25%인 테스트는 3번의 재시도를 허용하면 통계적으로 약 99.6%의 확률로 통과하게 됩니다. 버그는 여전히 그곳에 있습니다. 파이프라인 (pipeline)이 단지 당신에게 그 사실을 말해주지 않게 되었을 뿐입니다.
Luo, Hariri, Eloussi, 그리고 Marinov의 2014년 연구는 51개의 오픈 소스 프로젝트에 걸친 201개의 실제 플래키 테스트 수정 사례를 분석했으며, 주요 원인이 무작위성 (randomness)이 아니라 동시성 (concurrency), 비동기 대기 (async waits), 그리고 테스트 순서 의존성 (test order dependency)임을 발견했습니다. 이것들은 특정 타이밍 하에서만 나타나는 결정론적 버그 (deterministic bugs)입니다. 이것이 바로 Carnegie Mellon의 PASTA 랩이 Fray를 구축한 이유입니다. Fray는 재실행이 우연히 버그를 찾아내기를 기대하는 대신, 스레드 스케줄링 (thread scheduling)을 의도적으로 제어하여 나쁜 인터리빙 (interleaving, 실행 순서 섞임)을 강제하는 도구입니다.
출처: https://www.googblogs.com/flaky-tests-at-google-and-how-we-mitigate-them/ 및 https://mir.cs.illinois.edu/lamyaa/publications/fse14.pdf
두 번째 실패: 아무것도 증명하지 못하는 단언 (assertion)
이러한 사례를 확인하기 위해 특별한 시스템이 필요하지는 않습니다. 다섯 개의 단언 (assertion)이 있고 메시지가 없는 테스트 메서드만 있으면 됩니다. 테스트가 실패하면 무언가 고장 났다는 사실은 알 수 있지만, 무엇이 고장 났는지는 알 수 없으며, 메서드의 나머지 부분은 무엇인지 알려주기 위해 실행조차 되지 않습니다.
2015년, UCL과 Sheffield의 연구진들은 이 현상의 더 광범위한 버전에 이름을 붙였습니다. Barr, Harman, McMinn, Shahbaz, Yoo는 IEEE Transactions on Software Engineering에 "소프트웨어 테스트에서의 오라클 문제: 조사 (The Oracle Problem in Software Testing: A Survey)"를 발표하며, 관찰된 동작이 올바른지 결정하는 것이 왜 그것을 유발하는 입력을 생성하는 것보다 테스트의 더 어려운 절반인지를 설명했습니다. 명세 (specification)나 계약 (contract)이 해당 판단을 자동으로 수행할 수 없을 때, 그 작업은 결과를 눈으로 직접 확인하는 인간의 몫으로 돌아갑니다. 이 논문은 이후 천 번 이상 인용되었으며 10년이 지난 지금까지도 참조점으로 남아 있는데, 이는 이 분야가 실제로 얼마나 멀리 나아갔는지를 말해줍니다.
이 현상의 일상적인 버전에도 이름이 있습니다. 어떤 것이 실제로 실패했는지 알려주는 메시지 없이 단언 (assert)들이 쌓여 있는 상태인 '단언 룰렛 (Assertion Roulette)'은 2000년대 초반에 분류된 초기 테스트 스멜 (test smells) 중 하나입니다. 18개의 오픈 소스 Java 프로젝트를 대상으로 한 실증적 연구에 따르면, 조사된 코드베이스에서 가장 흔한 스멜인 이것이 JUnit 테스트 클래스의 약 62%에서 발견되었습니다. 오픈 소스 Android 앱을 대상으로 한 별도의 대규모 연구에서도 그 수치는 50% 이상으로 나타났습니다.
출처: https://www0.cs.ucl.ac.uk/staff/M.Harman/tse-oracle.pdf 및 https://www.cs.loyola.edu/~binkley/papers/icsm-12-smells.pdf
세 번째 실패: 아무것도 기억하지 못하는 에이전트 (agent)
여기서 저의 실제 업무가 시작됩니다. 제가 평가하는 모든 코딩 에이전트 (coding agent)는 이전 세션에 대한 기억이 전혀 없는 상태로 각 세션을 시작합니다. 지난주에 어떤 컨벤션 (convention)에 대해 교정받았는지, 이미 한 번 저질렀던 실수가 무엇인지, 이미 건드렸던 파일이 무엇인지 전혀 기억하지 못합니다. 그래서 처음부터 다시 유도 (re-derive)하고, 때로는 이미 수정했던 버그를 다시 도입하며, 그러고도 실행 결과를 성공이라고 보고합니다.
이것은 학습 (training)의 실패가 아니라, 아키텍처 (architectural) 상의 사실입니다. 대규모 코드베이스를 위한 코드화된 컨텍스트 인프라 (codified context infrastructure)에 관한 2026년 논문을 포함하여, 에이전트 메모리 (agent memory)에 관한 최근 조사 연구들은 이 분야의 모든 실무자가 결국 스스로 재발견하게 되는 동일한 점을 지적합니다. 즉, 상태 비저장 (stateless) 모델은 현재 아무리 유능하더라도, 그 목적을 위해 구축된 명시적인 메모리 레이어 (memory layer) 없이는 지난 세션에서 배운 것을 앞으로 전달할 수 없다는 것입니다. 이것이 바로 지속적인 프로젝트 컨텍스트 파일 (persistent project context file)과 같은 비공식적인 컨벤션이 에이전트 기반 코딩 도구들 사이에서 유행하게 된 실제 이유입니다. 아무도 상태 비저장성 (statelessness) 문제를 해결하지 못했습니다. 모두가 독립적으로 동일한 벽에 부딪혔고, 동일한 임시방편 (workaround)을 만들어냈을 뿐입니다.
두 번째 AI가 판사 (judge)로서 루프 (loop)에 진입하면 상황은 더욱 흥미로워집니다. 최근의 평가 신뢰성 (evaluation reliability) 연구에 따르면, 여러 LLM 판사들이 정확히 동일한 출력을 나란히 검토하더라도, 판사 자체에 내재된 무작위성 (randomness)을 고려할 때 이들 간의 일치도, 그리고 인간 평가자와의 일치도가 매우 불일치한다는 것을 보여줍니다. 이 때문에 연구자들은 현재 표준적인 평가자 간 신뢰도 (inter rater metrics) 지표가 실제 판단이 얼마나 불안정한지를 과소평가하고 있다고 주장합니다. 제가 따르는 루브릭 (rubric)이 무승부를 금지하고 모든 판단에 인용된 줄 번호를 요구하는 이유가 바로 이것입니다. 규칙이 우아해서가 아니라, 이 규칙이 없다면 "검증됨 (verified)"이라는 말은 조용히 "대충 훑어봤는데 괜찮아 보임"으로 변질되기 때문입니다.
출처: https://arxiv.org/pdf/2602.20478 및 https://arxiv.org/html/2412.12509v2
같은 거짓말, 세 가지 설정
같은 거짓말, 세 가지 설정
CI 파이프라인 (CI pipeline), JUnit 테스트 클래스 (JUnit test class), 그리고 AI 평가 실행 (AI evaluation run)은 겉보기에는 전혀 닮지 않았습니다. 하지만 그 이면에서는 동일한 방식으로 실패합니다. 각각은 통과(pass) 또는 실패(fail), 좋음(good) 또는 나쁨(bad)이라는 단 하나의 비트(bit) 형태의 출력을 생성하며, 이 비트가 실제로는 아무도 신중하게 내리지 않은 판단을 대신하게 만듭니다. 첫 번째 사례에서는 재시도 (Retries)가 이를 숨깁니다. 두 번째 사례에서는 라벨이 없는 단언문 (Unlabeled asserts)이 이를 숨깁니다. 세 번째 사례는 무상태성 (Statelessness) 때문에 상황이 더 악화되는데, 이미 무엇을 틀렸는지에 대한 기억이 없는 시스템은 자신이 같은 실수를 반복하고 있다는 사실을 알아챌 방법이 없기 때문입니다.
실제로 도움이 되는 것
사후에 덧붙여진 더 똑똑한 검사기가 아닙니다. 매일 이와 정확히 일치하는 실패 유형을 채점하며 얻은 제 경험상, 실제로 상황을 개선한 것은 "어떻게 아는가"라는 질문에 대해 "통과했다"라는 답변을 완전한 답으로 받아들이기를 거부하는 것이었습니다. 파일을 요구하십시오. 줄(line)을 요구하십시오. 리뷰어(reviewer)가 인간이든 모델(model)이든, 단순히 판결(verdict)을 내리는 대신 그들의 작업 과정(show their work)을 보여줄 것을 요구하십시오. 이는 더 느립니다. 하지만 이것만이 의미를 갖는 유일한 '그린(green, 통과)' 상태입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기