
테스트 통과(Green)가 더 나은 소프트웨어를 의미하지는 않는다
요약
테스트 통과(Green)가 반드시 소프트웨어의 개선을 의미하지는 않습니다. CI 테스트는 명세 준수와 정확성을 보장할 뿐, 지연 시간 감소나 사용자 활성화와 같은 실제 시스템 성능 및 비즈니스 지표의 개선을 측정하지 못한다는 점을 지적합니다.
핵심 포인트
- 테스트 통과는 정확성(Correctness)을 보장하지만 개선(Improvement)을 보장하지 않음
- CI는 명세 준수(Conformance)를 측정하며, 실제 결과(Effects)를 측정하지 못함
- 정확한 변경이 시스템 성능을 악화시킬 수도 있음
- 테스트 통과는 개선을 위한 필요조건일 뿐 충분조건은 아님
당신은 다음 변경 사항의 비용을 낮추기 위해 일주일 내내 리팩터링(refactor)에 매달렸습니다. 테스트는 통과(green)하고, PR(Pull Request)은 머지(merge)되며, 그것으로 아무도 더 이상 생각하지 않습니다. 다음 변경 사항의 비용이 실제로 낮아졌는지 확인하러 돌아오는 사람은 아무도 없습니다. 초록색 바(green bar)는 코드가 명세(spec)를 준수한다고 말해주었고, 모두는 그것을 완료된 것으로 받아들였으며, 당신이 애초에 그 작업을 수행했던 이유는 측정되지 않은 채 남겨졌습니다.
초록색(Green)이 실제로 확인하는 것
당신의 CI(Continuous Integration)는 초록색입니다. 테스트는 assert response.status == 200을 단언(assert)하고, 이를 통과합니다. 하지만 방금 배포한 캐싱 레이어(caching layer)가 p99 지연 시간(latency)을 줄였는지, 문구를 수정한 온보딩(onboarding) 단계가 활성화(activation)를 높였는지, 혹은 프롬프트(prompt) 변경이 에이전트(agent)가 지시를 더 자주 따르게 만들었는지는 단언하지 않습니다.
초록색 바는 당신이 묻지도 않은 질문에 답했습니다.
그 초록색이 실제로 당신에게 말해준 것은 무엇일까요? 그 변경 사항이 명세(spec)를 준수한다는 것 — 즉, 입력값이 단언된 출력값에 매핑되고, 계약(contract)이 유지되며, 당신이 테스트를 작성한 부분에서 퇴보(regression)가 발생하지 않았다는 것입니다. 그것은 정확성(correctness)입니다. 그것은 실재하며 필수적입니다. 하지만 그것은 시스템이 더 좋아졌는지에 대해서는 아무것도 말해주지 않았습니다. 당신은 바가 초록색이었기 때문에 배포했고, 그 바는 당신이 변경 사항을 배포하려 했던 목적을 전혀 측정하고 있지 않았습니다.
지연 시간(latency), 활성화(activation), 준수(adherence) 수치들은 결과(effects)입니다. CI는 결과를 측정하지 않습니다. CI는 준수(conformance)를 측정하며, 우리는 초록색 바가 다른 질문에도 답한 것처럼 해석해 버립니다.
침묵 속의 가정은 "통과(passes)"와 "더 나음(better)"이 동일한 축이라고 생각하는 것입니다. 즉, 정확한 변경은 그 사실만으로 개선(improvement)이라고 간주하는 것이죠. 이 둘은 동일한 축이 아닙니다. 완벽하게 정확한 변경이 시스템을 더 나쁘게 만들 수도 있고, 완벽하게 정확한 변경이 모든 지표를 이전과 똑같이 유지할 수도 있습니다. 테스트 스위트(suite)는 두 경우 모두 초록색으로 표시됩니다. 초록색은 "더 나음"을 위한 필요조건이지만 충분조건은 결코 아니며, 이 차이를 측정하는 팀은 거의 없습니다.
라이벌로 묘사되는 두 가지 규율
업계는 이미 이 두 가지 측면 모두에 이름을 붙였습니다. 그리고 그것들을 라이벌로 명명했습니다.
Spec-Driven Development는 다음과 같이 말합니다: 명세(specification)를 먼저 작성하고, 그에 맞춰 빌드한 다음, 빌드가 명세에 부합하는지 증명하십시오. 테스트 스위트(test suite)는 명세의 실행 가능한 형태입니다. 정확성(Correctness)이 게임의 전부입니다. SDD는 규율 있고 감사 가능하며, 대부분의 엔지니어링 조직이 실제로 운영하는 방식입니다. 하나의 흐름으로서, SDD는 기준이 통과(green)되는 순간 종료됩니다.
그것이 어디서 멈추는지 주목하십시오. 그것은 '정확함(correct)'에서 멈춥니다. 해당 루프 내의 그 어떤 것도 배포된 변경 사항이 무언가를 개선했는지 묻지 않습니다.
Hypothesis-Driven Development는 정반대의 질문을 던집니다. Thoughtworks는 이를 세 가지 요소로 정의했습니다: '우리는 X가 결과 Y를 초래할 것이라고 믿는다'; '우리는 측정 가능한 신호 Z를 확인했을 때 우리가 옳았음을 알게 될 것이다'. 여기서 진보의 단위는 통과된 빌드가 아니라, 검증된 학습(validated learning)입니다. 효과를 예측하고, 배포하고, 측정하며, 그 측정 결과가 당신이 옳았는지를 알려줍니다. PMI의 기대치 관리(expectation-management) 문헌도 다른 관점에서 같은 이야기를 합니다: 기대치(expectation)란 소유자와 마감일이 있는 관리 대상이며, 나중에 발견되기보다 조기에 추적되고 드러나야 하는 객체입니다. 하나의 흐름으로서, 이는 SDD가 끝나는 지점인 '예측'에서 시작하여 '배포(ship)'를 넘어 실행됩니다.
이것들은 서로 경쟁하는 철학으로 제시되곤 합니다. 즉, 명세 우선(spec-first) 대 가설 우선(hypothesis-first), 올바름 증명(prove-correct) 대 더 나음 증명(prove-better)의 대결이며, 어느 한 진영을 선택하라고 합니다. 두 가지 흐름을 다시 살펴보십시오. 하나는 '올바름(correct)'에서 끝납니다. 다른 하나는 '예측됨(predicted)'에서 시작하여 '더 나음(better)'에서 끝납니다. 이들은 서로 다른 두 가지 질문에 답하고 있으며, 진영을 선택하라는 프레임워크는 이 질문들이 하나라고 가정합니다. Thoughtworks의 HDD와 PMI의 기대치 관리(expectation-management)는 제가 여기서 새로 주장하는 것이 아닙니다. 이는 수십 년간 존재해 온 선행 기술(prior art)이며, 저는 이를 검증 수단으로 인용하고 있습니다. 하지만 그들 중 누구도 이 두 가지 흐름이 결코 동일한 질문을 겨냥하고 있지 않았다는 점을 알아차리지 못했습니다.
직교성(Orthogonality)의 움직임
두 가지 서로 다른 질문은 두 가지 서로 다른 축을 의미합니다. 한 축을 측정함으로써 다른 축에 답할 수는 없습니다. 그리고 단일한 통과/실패(pass/fail) 기준은 오직 첫 번째 질문에 답하기 위해 만들어졌습니다. '올바름(Correct)'은 한 축에 위치하며, '더 나음(better)'은 그와 수직인 축에 위치합니다.
한 축은 명세 준수(spec-conformance)입니다. 즉, 변경 사항이 명세된 대로 동작하는가? 하는 문제입니다. 통과 혹은 실패이며, pytest가 이미 이에 대해 답을 줍니다. 다른 축은 효과 판결(effect-verdict)입니다. 즉, 변경 사항이 움직이기로 되어 있던 지표(metric)를 실제로 움직였는가? 하는 문제입니다. 확인됨, 반박됨, 또는 아직 알 수 없음 — 현재 귀하의 파이프라인(pipeline) 중 그 어느 것도 이에 답해주지 못합니다.
이를 2×2 매트릭스에 배치해 보십시오.
| | **효과 확인됨 (effect confirmed)** | **효과 반박됨 / 측정되지 않음 (effect refuted / unmeasured)** |
|-------------------------|------------------------------------------|--------------------------------------------------------------|
...
이 사분면 중 세 곳은 익숙합니다. 효과가 어느 방향으로 나타나든 빨간색(Red)은 머지(merge)를 차단합니다. 초록색이면서 확인된 상태(Green-and-confirmed)는 귀하가 원했던 승리입니다. 아무도 이름을 붙이지 않는 사분면은 우측 상단입니다: 초록색이지만 더 낫지는 않음 (green but no better). 변경 사항은 올바르고, 명세를 준수하며, 배포(shipped)되었습니다. 하지만 시스템을 개선하지 못했거나, 혹은 확인조차 하지 않았습니다. 시스템의 관점에서 보면 이 둘은 같은 것입니다.
그 사분면은 실제로 배포된(shipped) 변경 사항 대부분이 존재하는 곳이지만, 이를 향한 유일한 측정 도구가 잘못된 축을 가리키고 있는 초록색 막대뿐이기 때문에 보이지 않습니다. 여기서 재사용 가능한 사고 모델(mental model)은 다음과 같습니다. "통과했는가?"를 마치 하나의 질문인 것처럼 묻는 것을 멈추십시오. 그것은 두 개의 축에 걸친 두 개의 질문이며, 당신은 그중 하나만을 측정하고 있을 뿐입니다.
실제 사례: 초록색 아키텍처 테스트 (green architecture tests)
모든 Python 팀이 인지하고 있는 사례로 구체화해 보겠습니다. 당신은 육각형 레이아웃(hexagonal layout)인 포트와 어댑터(Ports and Adapters) 패턴을 채택했습니다. 순수 코어(contract/, dto/, policy/)는 표준 라이브러리(standard library)만을 임포트(import)합니다. 어댑터(Adapters)는 I/O를 래핑(wrap)합니다. 서브시스템(Subsystems)은 어댑터를 조합합니다. 의존성 규칙(dependency rule)은 단 한 문장입니다: 의존성은 안쪽을 향하며, 순수 코어는 아무것도 향하지 않습니다.
당신은 모든 커밋(commit)마다 실행되는 아키텍처 테스트를 통해 이를 강제합니다:
def test_core_purity():
for module in pure_core_modules():
assert no_imports(module, {"yaml", "requests", "subprocess", "open"})
테스트는 통과(green)되었습니다. 모든 순수 모듈은 임포트가 깨끗합니다. 의존성 그래프(dependency graph)는 규칙을 준수하며, 매 푸시(push)마다 기계적으로 올바름이 증명됩니다.
이제 다른 질문을 던져보십시오. 당신이 육각형 레이아웃을 채택한 것은 그 자체를 위해서가 아니었습니다. 당신은 다음과 같은 주장(claim) 때문에 채택했습니다: 변경 비용이 저렴해지고, 폭발 반경(blast radius)이 줄어들며, 단 하나의 모의 객체(mock) 없이도 코어를 테스트할 수 있게 될 것이라는 주장 말입니다. 실제로 그렇게 되었습니까? test_core_purity는 이를 알려줄 수 없습니다. 이 테스트는 임포트 그래프의 형태를 측정할 뿐, 그 형태가 이득을 가져다주었는지는 측정하지 못합니다. 여기서 초록색(green)은 '준수함(conformant)'을 의미합니다. 아무것도 더 저렴하게 만들지 못한 준수하는 아키텍처는, 바로 그 지점을 정면으로 가리키는 .py 파일이 존재하는 '초록색이지만 더 나아지지는 않은(green-but-no-better)' 사분면입니다.
이것이 하나의 테스트 파일에 담긴 논지(thesis)의 전부입니다. 아키텍처 테스트는 사양(spec) 축에 존재합니다. 리팩터링(refactor)을 설득했던 약속은 효과(effect) 축에 존재합니다. 이 둘은 서로 맞닿지 않으며, 오직 하나에만 측정 도구가 있을 뿐입니다.
기대치 원시 개념 (The expectation primitive)
만약 "더 나음(better)"이 별도의 축이라면, 그에 맞는 별도의 측정 도구가 필요합니다. 그 도구는 바로 기대치(expectation)입니다. 즉, 위에서 언급한 HDD 흐름을 일급 객체(first-class artifact)로 만든 것입니다.
기대치(expectation)는 변경 범위가 지정되고(change-scoped), 반증 가능하며(falsifiable), 마감 기한을 포함하는(deadline-carrying) 예측입니다. 이는 HDD 트리플(triple) — X를 믿고, 결과 Y를 기대하며, 기준 Z로 이를 확인한다 — 을 아무도 다시 열어보지 않는 기획 문서 속에 두는 대신, 변경 사항과 함께 이동하는 하나의 산출물(artifact)로 만든 것입니다. 이는 기준점(baseline), 경계가 있는 측정 뷰(bound measurement view) — 해당 변경 사항과 연결되어 메트릭을 읽어오는 쿼리(query) — 임계값(threshold), 그리고 마감 기한을 포함합니다. 구체적으로는 다음과 같습니다: "캐싱 변경 사항은 지난주 기준점과 비교하여 p99 지연 시간(latency)을 200ms 미만으로 줄여야 하며, 금요일에 재확인한다." 이 문장 자체가 하나의 산출물이며, 디프(diff)에 부착되어 함께 이동합니다.
여기서 진정으로 새로운 부분이자, 제가 정확히 짚고 넘어가고 싶은 부분은 다음과 같습니다: 판정(verdict)은 시스템에 의해 측정된다는 점입니다.
HDD와 PMI 모두 이미 소유자(owner)와 마감 기한을 가진 반증 가능한 예측을 만들라고 말해왔습니다. 그것은 진보가 아닙니다. 진보는 예측이 실제 측정 뷰(measurement view)와 결합되고, 시스템이 해당 뷰를 읽어 스스로 판정을 내린다는 점입니다. 예측을 했던 사람이 다시 돌아와 점수를 매기지 않습니다. 인간은 검증 루프(verification loop)에서 벗어납니다.
이것은 대시보드 알림(dashboard alert)이 아닙니다. 대시보드 알림은 특정 변경 사항과 무관하게, 메트릭이 특정 선을 넘을 때 발생합니다. 기대치는 하나의 예측을 하나의 변경 사항에 결합하고 _그 예측_에 대해 점수를 매깁니다. 대시보드는 지연 시간이 상승했다고 알려주지만, 기대치는 지연 시간을 줄일 것이라고 예측했던 변경 사항이 오히려 반대의 결과를 냈음을 알려줍니다.
판정 어휘는 세 단어로 이루어져 있으며, 핵심적인 규율은 세 번째 단어에 있습니다:
confirmed(확인됨) — 측정된 값이 임계값을 충족했습니다. 자동으로, 조용히 해결됩니다. 이를 공표하는 것은 불필요한 정보(filler)일 뿐입니다.refuted(반박됨) — 측정된 값이 임계값을 맞추지 못했습니다. 기대치는 열린 상태로 유지되며, 예측된 차이(delta)와 실제 값이 나란히 표시됩니다.inconclusive(결론 미정) — 측정값이 아직 스칼라(scalar) 값을 생성하지 못했습니다. 상태는 열린 채로 유지되며 표시됩니다. 이는 결코 '확인됨'으로 해석되지 않습니다. "측정하지 않았다"는 것은 "성공했다"는 뜻이 아닙니다.
그 세 번째 단어는 대부분의 대시보드가 조용히 누락시키는 단어입니다. 아직 등급을 매길 수 없는 예측은 통과(pass)가 아닙니다. inconclusive(결론을 내릴 수 없음)를 confirmed(확인됨)와 별개로 유지하는 것이, '초록색이지만 더 나아지지는 않은' 사분면이 다른 이름으로 다시 채워지는 것을 막는 방법입니다.
셀렉터(selector)는 변경 사항의 형태(shape)가 아니라 그 효과(effect)에 따라 경로를 지정합니다. 한 줄짜리 차이(diff)가 크고 측정 가능한 효과를 가져오며 기대를 충족해야 할 수도 있는 반면, 천 줄짜리 리팩터링(refactor)은 순수하게 규정 준수(conformance)일 뿐 아무런 기대도 충족할 필요가 없을 수 있습니다. 그리고 당신은 배포할 때 경로를 선택하는 것이지, 배포한 후에 선택하는 것이 아닙니다.
동일한 육각형 프로젝트(hexagonal project)에 대해 셀렉터를 실행해 보십시오. test_subsystem_isolation 실패를 해결하기 위해 모듈의 위치를 재조정하는 것은 형태의 변화(shape change)입니다. 이는 경계를 복구하지만, 그 효과는 측정이 어렵고, 초록색 테스트 결과 이상의 어떠한 기대도 충족할 필요가 없습니다. 반면 더 저렴한 변경을 '약속했던' 리팩터링은 다른 경로에 속합니다. 그것은 측정 가능한 주장을 했으므로 기대를 충족해야 합니다: 즉, '다음 아티팩트 클래스를 추가할 때 세 개 이하의 파일만 수정되어야 하며, 다음 작업이 들어올 때 재확인한다'는 식입니다. 동일한 코드베이스에서 두 가지 변경이 일어났지만, 두 개의 경로가 존재합니다.
이것이 바로 명세(spec) 대 가설(hypothesis)의 논쟁이 항상 놓쳤던 지점입니다. 셀렉터는 SDD(Specification-Driven Development)와 HDD(Hypothesis-Driven Development) 사이에서 승자를 가리는 것이 아닙니다. 셀렉터는 조건부입니다. 효과 측정이 어려운 변경에는 SDD의 규율을 적용하고, 효과를 측정할 수 있는 변경에는 HDD의 규율을 적용하며, 각 변경에 맞춰 선택합니다. 두 진영을 화해시키는 규율은 각 진영이 언제 옳은지를 아는 라우팅 규칙(routing rule)의 형태를 띱니다.
한 가지 경계해야 할 함정이 있습니다. 단지 선택자(selector)를 만족시키기 위해 측정할 수 없는 변화에 기대를 부여하지 마십시오. 실제 임계값(threshold)이 없는 예측은 가면을 쓴 순응(conformance)일 뿐이며, 이는 모든 이들이 판결을 무시하도록 가르치는 꼴이 됩니다. 만약 효과를 측정할 수 없다면, '없음'이라고 선언하십시오. refuted(기각됨)와 inconclusive(결론을 내릴 수 없음) 판결의 정직함은 선택자가 "이 변화는 아무런 기여도 하지 않았다"라고 말할 용기가 있는지에 달려 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기
