뮤테이션 효능 100%, 뮤턴트 사망률 0: 아무것도 주장하지 않는 게이트
요약
뮤테이션 테스트의 효능 계산 방식에서 발생하는 오류와 그 위험성을 경고합니다. 시간 초과(timeout)를 사망(killed) 사례로 잘못 포함하여 실제 결함을 잡지 못함에도 100%의 완벽한 점수를 보고하는 품질 게이트의 허점을 지적합니다.
핵심 포인트
- 뮤테이션 테스트 점수 계산 시 시간 초과를 사망으로 포함하면 지표가 왜곡됨
- 결함을 전혀 잡지 못해도 100% 효능을 보고하는 가짜 품질 게이트 발생 가능
- 테스트 어설션이 작동하지 않아도 게이트가 '녹색'을 유지하는 위험성
- 신뢰할 수 있는 품질 지표를 위해 시간 초과와 사망 사례를 엄격히 구분해야 함
완벽한 점수를 보고하지만 아무것도 잡아내지 못하는 품질 게이트는 아예 없는 게이트와 구별할 수 없으며, 애초에 방어하기 위해 만들어진 회귀(regression)를 잡을 수도 없습니다.
헤드라인에는 effective_test_efficacy = 100.0000%라고 적혀 있었습니다. 그 아래에는 mutants_killed = 0, mutants_lived = 0, mutants_timed_out = 603이 표시되었습니다. 이 완벽한 점수는 산술적인 계산에 의한 것이었습니다 — (사망한 뮤턴트 + 시간 초과) / (사망한 뮤턴트 + 살아남은 뮤턴트 + 시간 초과) = 603 / 603 = 100%. 보안이 중요한 패키지에 있는 603개의 뮤턴트 중 단 하나도 테스트 어설션(assertion)이 발동하여 잡아낸 것이 없었습니다. 이 게이트는 품질 개선을 위해 병합되려 하고 있었습니다.
품질 게이트가 동시에 100%를 보고하고 아무것도 주장할 수 있을까요?
네, 그리고 이것이 그랬습니다. 뮤테이션 게이트는 (사망한 뮤턴트 + 시간 초과) / 전체 점수를 계산하기 때문에, 모든 것이 시간 초과되고 사망한 뮤턴트는 0인 실행은 평평하게 100%를 기록하면서도 테스트가 결함(fault)을 포착했다는 증거가 전혀 없다는 것을 보여줍니다. 이 완벽한 숫자는 어떠한 테스트 품질 정보도 담고 있지 않습니다. 이는 하네스(harness)가 단 한 번도 어설션이 발동하는 것을 지켜보지 못했다는 가장 큰 신호입니다.
병합을 막았어야 할 숫자
같은 패키지는 3개월 전에 이미 20개의 뮤턴트를 사망시킨 적이 있었습니다. 새로운 하네스는 여전히 완벽한 점수를 보고하면서 이를 0으로 퇴보시켰습니다. 아무것도 잡아내지 못하는 게이트는 모든 것을 잡아내는 게이트와 동일한 점수를 보고하며, 이것이 함정입니다.
간단 복습: 뮤테이션 테스트(mutation testing)는 의도적으로 코드를 망가뜨립니다 — 비교를 뒤집거나, 리턴을 삭제하는 식으로요 — 그런 다음 테스트를 실행합니다. 만약 테스트가 실패하면, 해당 뮤턴트는 _사망(killed)_합니다: 이는 여러분의 테스트가 그 버그를 포착할 것이라는 의미입니다. 만약 테스트가 여전히 통과한다면, 뮤턴트는 _생존(lived)_합니다: 이것은 실제 결함이 있다는 뜻입니다. 이 점수는 여러분의 테스트가 결함을 감지할 수 있는지 측정합니다.
_시간 초과(timeout)_는 사망한 것도 생존한 것도 아닙니다. 이는 변형된 프로세스가 시간 제한을 넘어서 실행되었음을 의미하며 — 여기서는 기준선의 3배 동안 — 어떤 테스트도 판결을 내릴 수 없었다는 뜻입니다.
따라서 603개의 시간 초과(timeouts)와 0개의 사망(kills)은 '완벽한 테스트'가 아닙니다. 이는 테스트를 수행할 기회조차 얻지 못하고 하네스(harness)가 포기한 횟수를 나타내며, 점수 계산 공식은 이 모든 것을 100%에 포함시킵니다. 이 지표는 테스트 자체에 대해서는 아무것도 알려주지 않지만, 완벽하다고 표시됩니다.
아무것도 주장하지 않는 게이트가 없는 게이트보다 더 나쁜 이유
이 게이트는 자신 있게 쓸모없습니다. 실제 신호가 있어야 할 자리를 차지하고 있으면서도 녹색(green)을 보여줍니다.
이 게이트가 잡아내도록 설계된 회귀(regression)를 상상해 보세요. 미래의 변경 사항에서 보안 패키지 테스트의 단언문(assertions)은 삭제되지만, 테스트 본체는 그대로 남아 있습니다. 코드는 여전히 실행되지만, 아무것도 결과를 확인하지 않습니다. 이 하네스에서는 해당 뮤턴트들이 여전히 시간 초과가 발생하므로, 유효 효능(effective efficacy)은 100%를 유지하고, '생존자 없음' 바닥값(floor)도 만족하며, 게이트는 녹색을 유지합니다. 게이트가 감지하도록 만들어진 정확한 '테스트는 실행되었지만 아무것도 잡아내지 못하는' 실패는 이 하네스에게 보이지 않습니다.
다른 방향에서도 상황은 더 나쁩니다. 게이트의 유일한 진정한 기능은 0개의 뮤턴트만 생존할 수 있다고 말하는 바닥값입니다. 하지만 만약 하네스가 무차별적으로 시간 초과가 발생한다면, 느리게 실행되는 진짜 생존자가 생존자로 분류되지 못하고 시간 초과로 오분류되어 — 결국 바닥값을 건드리지 못하게 됩니다. 게이트는 자신이 포착해야 할 바로 그 생존자들에 대해서 눈이 멀어 있습니다.
아무것도 주장하지 않는 녹색 게이트는 게이트가 없는 상태와 구별할 수 없습니다. 차이점은 팀이 녹색인 쪽을 신뢰한다는 것입니다.
역사가 이것을 결함(quirk)이 아닌 회귀로 만든 이유
원래부터 점수가 이렇지는 않았습니다. 패키지 자체의 기록은 3개월 전에 건강한 실행 결과를 보여주었습니다: killed = 20, lived = 0, timed_out = 310. 20개의 실제 사망(kill)이 있었습니다. 현재 실행 결과는: killed = 0, timed_out = 603. 실제 사망 횟수는 20개에서 0개로 떨어졌고, 시간 초과는 대략 두 배가 되었습니다.
이것은 'fail-closed 코드가 자연스럽게 타임아웃을 발생시킨다'는—개별 뮤턴트에 대한 증거가 없는 단순한 주장일 뿐인— 것과, '하네스(harness) 리라이트에서 무언가가 감지하는 것을 멈췄다'는 것 사이의 차이입니다. 잘 테스트된 보안 코드의 정상적인 실행은 주로 kills로 지배됩니다. 킬(kills)이 없고 타임아웃만 600개가 넘는 실행은 깨진 측정기일 뿐, 건강한 상태를 의미하지 않습니다. 기준선 노트에서는 타임아웃이 괜찮다고 주장했습니다. 단 하나의 뮤턴트가 빠르게 실패하는 대신 타임아웃된 이유에 대해서는 결코 보여주지 않았습니다.
Filed-as-resolved가 해결되었다고 볼 수 없는 이유
여기에 메타 교훈(meta-lesson)이 있습니다. 이것은 뮤테이션 테스트를 넘어 일반화되는 부분입니다. 이 게이트와 관련된 작업 항목은 _해결됨(resolved)_으로 표시될 예정이었습니다. 거버넌스 구조는 훌륭했습니다—JSON 보고서, 확정된 기준선, 엄격성 예산에 대한 등록, 승진 경로까지 갖추었습니다. 작업은 약 80% 정도 진행되었습니다.
부족했던 나머지 20%만이 중요했습니다: 즉, 이 게이트가 실제로 신호를 보내는지에 대한 증거였습니다. _스코어링 스크립트(scoring script)_가 합성된 생존 뮤턴트 보고서를 받으면 실패한다는 것을 주장하는 단위 테스트가 있었습니다. 하지만 _실제 패키지에 적용된 실제 하네스_가 게이트가 포착할 수 있는 생존자를 실제로 생성한다는 증거는 없었습니다—왜냐하면 유일한 실제 실행 결과가 0이었기 때문입니다. 게이트의 단위 테스트가 통과했다는 것은 스코어러(scorer)가 작동한다는 것을 알려줄 뿐, 이 게이트가 라이브 코드의 회귀를 포착할 수 있다는 것을 알려주지는 않습니다.
Filed-as-resolved는 해결된 것이 아닙니다. 퇴행적인 실행을 프로젝트의 품질 기준으로 확정하는 것은 품질 개선이 아니라 아무것도 연결되지 않은 녹색 신호등과 같습니다. 올바른 마감은 시드된 생존자 증명(seeded-survivor proof)을 필요로 합니다: 즉, 하나의 보안 패키지에 의도적으로 fail-open 상태를 도입하고, 게이트를 실행하여 그 뮤턴트가 _생존했다(lived)_는 것을 보여주고 빨간색으로 종료시키는 것입니다. 이 '빨강 후 녹색' 시연이 존재할 때까지, 이 게이트는 증명된 것이 아니라 주장만 된 것이므로—작업 항목은 열린 상태로 유지됩니다.
타임아웃 위주의 실행이 허용되는 경우
솔직히 말해서, 모든 타임아웃이 측정 장치의 고장은 아닙니다. 실제로 느린 코드—무거운 암호화(crypto), 네트워크 왕복 시간(network round-trips), 반환하는 대신 차단되는 의도적인 실패 폐쇄 경로(deliberate fail-closed paths)—에 대한 뮤테이션 테스트는 실제 타임아웃을 발생시킬 것이며, 각각의 타임아웃을 명확하게 종료시키는 과정은 그 신호가 가치 있는 것보다 더 많은 비용이 들 수 있습니다. 만약 특정 타임아웃이 코드가 느리지만 올바른 작업을 수행하고 있다는 _뮤턴트별 증거(per-mutant evidence)_를 가지고 있다면, 이를 '생존하지 않음'으로 분류하는 것은 방어할 가치가 있습니다.
절대 용납될 수 없는 것은 이 게이트가 내린 추론입니다. 뮤턴트별 증거 없이 대량의 타임아웃을 발생시키고, 이를 종료된 것과 동일하게 취급하며, 실제 종료 기록이 아무도 모르게 그것들로 대체되는 것입니다. 경계는 바로 그 증거에 있습니다. 뮤턴트별로 설명할 수 있는 타임아웃은 측정치이지만, 603개의 설명되지 않은 타임아웃으로 구성된 100%는 완벽한 점수를 가장한 추측일 뿐입니다. 그리고 아예 점수에 대해 논쟁하는 것보다 더 저렴한 방법이 있습니다. 만약 패키지의 테스트가 정직하게 뮤테이션 테스트를 수행하기에 너무 느리다면, 그렇게 말하고 명시적으로 제외하세요. 제외된 패키지는 눈에 띄지만, 퇴화된(degenerate) 패키지는 그렇지 않습니다.
제가 모든 게이트에 붙여둘 규칙은 이렇습니다. 녹색 실행(green run)에서 게이트 작업을 종료하지 마세요. 빨간색으로 변하는 것을 먼저 지켜본 실행에서 종료하세요. 생존자를 심고, 그 게이트가 그것을 잡아내는 것을 증명한 다음, 통과시키세요. 게이트의 유일한 임무는 무언가 잘못되었을 때 빨간색이 되는 것입니다. 이 게이트는 완벽한 점수를 보고하면서 이 목적을 뒤집었습니다.
이로써 시리즈가 마무리됩니다. 다섯 가지 실패 유형과 하나의 형태: 코드는 컴파일되었고, 테스트는 통과했으며, 점수는 완벽하게 읽혔지만 — 검증은 여전히 포착하도록 설계된 것을 놓쳤습니다. 파트 1은 형제 작업(sibling work)을 잘못된 커밋으로 쓸어 담았던 git 플래그였습니다. 파트 2는 존재하는 것만 확인하고 빠진 것은 결코 확인하지 않았던 네 번의 검토였습니다. 파트 3은 모든 테스트를 통과했지만 조용히 연산을 제거했던 리라이트(rewrite)였습니다. 파트 4는 존재했지만 연결되지 않았던 코드였습니다. 이번 마지막이자 가장 조용한 사례는 이 모든 것을 지켜보면서 아무것도 잘못되었다고 보고하지 않는 게이트입니다. 왜냐하면 아무것도 검사되지 않았기 때문입니다. AI는 병목 현상을 코드를 작성하는 것에서 그것을 검증하는 것으로 옮겼습니다. 이 시리즈의 모든 것은 그 간극(gap)에서 깨졌으며, 모든 수정은 같은 움직임이었습니다 — 녹색(green)이 스스로를 증명하도록 만드는 것입니다.
녹색으로만 본 게이트는 작동하는 것을 본 적 없는 게이트입니다.
AmanERP
AmanERP는 SMB를 위한 AI 네이티브 ERP로, 우리가 신뢰하기 전에 빨간색으로 변했던 품질 게이트 뒤에서 공개적으로 구축되었습니다. _Aman_은 평화를 의미하며 — 정직하게 측정된 차분한 소프트웨어입니다. www.amanerp.com
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기