개념
명세를 보는 눈에서 코드를 보는 눈으로
지금까지 배운 기법(동등분할, 경계값, 상태전이, 결정테이블, 유즈케이스, 페어와이즈, 분류 트리)은 전부 명세 기반 이다 — 코드를 한 줄도 안 보고 만들 수 있다. 구조 기반 기법은 반대다. 실제 코드의 구조를 보고, 그 구조가 얼마나 실행됐는지를 기준으로 테스트를 설계하거나 평가한다.
비유하자면 이렇다. 명세 기반 기법은 여행 계획서를 보고 "가기로 한 곳을 다 갔는가"를 확인하는 일 이다. 구조 기반 기법은 지도를 펼쳐놓고 "이 도시의 길 중 우리가 밟지 않은 길은 어디인가"를 확인하는 일 이다. 계획서에 없던 골목이 지도에는 있을 수 있고, 사고는 대개 그 골목에서 난다.
여기서 아주 중요한 전제를 먼저 못박아 두자. 커버리지는 테스트를 만들어내는 도구가 아니라, 이미 만든 테스트를 평가하는 자다. 커버리지 숫자를 보고 "여기가 비었네"를 알 수는 있지만, 그 빈칸을 무엇으로 채워야 하는지는 커버리지가 말해주지 않는다. 채우는 것은 여전히 명세 기반 기법과 사람의 판단이다. 이 구분을 놓치면 이 레슨의 마지막 절("커버리지 100%가 결함 0%가 아닌 이유")이 왜 그렇게 중요한지 이해할 수 없다.
커버리지는 무엇을 재는 숫자인가
커버리지 도구는 마술이 아니다. 하는 일은 단순하다. 코드의 각 줄과 각 분기에 "여기 지나갔음"을 기록하는 카운터를 심어 두고(계측, instrumentation), 테스트를 돌린 뒤 카운터를 세는 것 이다.
그래서 커버리지가 말해줄 수 있는 것과 없는 것이 처음부터 정해져 있다.
| 커버리지가 아는 것 | 커버리지가 모르는 것 |
|---|---|
| 이 줄이 실행됐다 | 그 줄의 결과가 맞는지 |
| 이 분기의 참·거짓이 각각 나왔다 | 그 분기의 조건이 기획서와 같은지 |
| 이 함수가 한 번도 호출되지 않았다 | 있어야 할 함수가 아예 없다는 것 |
오른쪽 열이 이 레슨의 절반이다. 왼쪽만 보고 안심하는 순간이 실무에서 사고가 나는 지점이다.
하나의 함수로 세 기준을 나란히 계산하기
말로 설명하면 다 비슷하게 들린다. 같은 함수 하나로 세 기준을 직접 계산 해 보면 차이가 확실해진다.
function getShippingFee(amount, isMember) {
if (amount >= 30000 && isMember) {
return 0; // ① 무료배송
}
return 3000; // ② 배송비 3000원
}이 함수에는 판정(decision)이 하나, 그 안에 개별 조건(condition)이 두
개(amount >= 30000, isMember) 있다. 가능한 조건 조합은 네 가지다.
| # | amount >= 30000 | isMember | 판정 결과 | 실행되는 줄 |
|---|---|---|---|---|
| 1 | 참 | 참 | 참 | ① |
| 2 | 참 | 거짓 | 거짓 | ② |
| 3 | 거짓 | 참 | 거짓 | ② |
| 4 | 거짓 | 거짓 | 거짓 | ② |
이제 세 기준이 각각 무엇을 요구하는지 보자.
구문 커버리지(Statement Coverage) — 모든 실행 가능한 줄이 최소 한 번씩 실행됐는가. ①과 ② 두 줄이 다 실행되면 된다. 조합 1번 하나(①)와 2·3·4번 중 아무거나 하나(②), 즉 두 개의 케이스면 100% 다.
분기 커버리지(Branch/Decision Coverage) — if 판정의 결과가 참·거짓 각각
한 번씩 나왔는가. 위와 같은 두 케이스로 참·거짓이 다 나오므로 여기서도 두
개면 100% 다. 이 함수처럼 if에 else가 없더라도 "판정이 거짓이라 아무것도
안 하고 지나간 경로"가 분기 하나로 세어진다는 점을 기억해두자.
조건 커버리지(Condition Coverage) — amount >= 30000과 isMember
각각이 참·거짓을 모두 거쳤는가. 조합 2번(참·거짓)과 3번(거짓·참) 두 개면
두 조건 모두 참·거짓을 다 겪는다. 역시 두 개면 100% 다.
여기서 이 레슨에서 가장 놀라운 사실이 나온다.
조건 커버리지 100%를 만족하는 그 두 케이스(2번·3번)는 판정 결과가 둘 다 거짓이다. 즉
return 0;(①)은 한 번도 실행되지 않았고, 분기 커버리지는 50%이며, 구문 커버리지도 50%다.
세 기준을 한 표로 정리하면 이렇다.
| 기준 | 최소 케이스 예시 | 그때 구문 | 그때 분기 | 그때 조건 |
|---|---|---|---|---|
| 구문 100% | (50000, true), (10000, true) | 100% | 100% | 50%(isMember가 계속 참) |
| 분기 100% | (50000, true), (10000, true) | 100% | 100% | 50% |
| 조건 100% | (50000, false), (10000, true) | 50% | 50% | 100% |
| 조건+분기 100% | 위 두 묶음의 합 = 3케이스 | 100% | 100% | 100% |
"더 엄격한 기준이 항상 약한 기준을 포함한다"는 직관이 여기서 깨진다. 조건 커버리지는 분기 커버리지를 보장하지 않는다. 그래서 실무 도구들은 조건 커버리지를 단독으로 쓰지 않고, 조건/판정 커버리지(Condition/Decision Coverage) — 개별 조건도 참·거짓을 다 겪고 판정 결과도 참·거짓을 다 겪어야 하는 기준 — 을 쓴다.
단축 평가가 커버리지를 한 번 더 흔든다
대부분의 언어에서 A && B는 A가 거짓이면 B를 아예 평가하지 않는다. 이걸
단축 평가(short-circuit)라고 한다. 위 표의 3번 조합(amount >= 30000이 거짓)에서
isMember는 값이 무엇이든 읽히지도 않는다.
QA에게 이게 왜 중요한가.
isMember를 만드는 코드가 예외를 던지는 코드였다면, 그 예외는 amount가 작은 경우엔 영원히 재현되지 않는다. "가끔만 터지는 버그"의 흔한 정체다.- 커버리지 도구는 "평가되지 않은 조건"을 미커버로 표시한다. 리포트에서 분기 퍼센트가 이상하게 낮게 나오는 이유가 대개 여기다.
- 개발자가
&&의 순서를 바꾸는 리팩터링(예:isMember && amount >= 30000)을 하면 동작은 같아 보이지만 실행 경로가 달라진다. 회귀 테스트에서 눈여겨볼 변경이다.
모듈 9(코드 읽기)에서 &&·||를 읽는 법을 다시 만나게 되는데, 그때 이 단축
평가를 떠올리면 "조건의 순서" 자체가 테스트 대상이 된다는 걸 알게 된다.
MC/DC — 왜 여기까지 가는가
MC/DC(Modified Condition/Decision Coverage) 는 조건/판정 커버리지에 한 가지를 더 요구한다.
각 개별 조건이, 다른 조건은 그대로 둔 채 자기 값만 바뀌었을 때, 판정 결과를 바꾼다는 것을 보이는 케이스 쌍이 있어야 한다.
말이 어려우니 위 표로 직접 골라보자. isMember가 독립적으로 영향을 준다는 걸
보이려면 — amount >= 30000은 고정한 채 isMember만 뒤집었을 때 판정이 바뀌는
쌍이 필요하다. 조합 1번(참·참 → 참)과 2번(참·거짓 → 거짓) 이 그 쌍이다.
amount >= 30000에 대해서는 1번과 3번(거짓·참 → 거짓) 이 쌍이 된다.
| 조건 | 독립성을 보이는 쌍 | 무엇이 증명되는가 |
|---|---|---|
| isMember | 1번 ↔ 2번 | 금액이 3만원 이상일 때, 회원 여부만으로 결과가 갈린다 |
| amount >= 30000 | 1번 ↔ 3번 | 회원일 때, 금액만으로 결과가 갈린다 |
두 쌍을 합치면 필요한 케이스는 1번, 2번, 3번 세 개 다. 4번(거짓·거짓)은 필요 없다. 조건이 n개일 때 MC/DC를 만족하는 최소 케이스 수는 보통 n+1개 로, 모든 조합을 다 도는 2ⁿ개보다 훨씬 적다. 조건이 10개면 1,024개가 아니라 11개다. MC/DC의 실용적 가치가 바로 이 지점 이다 — 전수 조합의 폭발을 피하면서도 "각 조건이 정말 쓸모 있는가"를 증명한다.
MC/DC가 잡아내는 대표적인 결함이 하나 있다. 아무 영향도 주지 않는 조건.
개발자가 if (a && b && (c || c))처럼 논리를 잘못 쓰거나, 요구사항이 바뀌면서
의미를 잃은 조건이 남아 있으면, 그 조건은 어떤 케이스로도 독립적 영향을 보일 수
없다. 즉 MC/DC를 만족시키려다 실패하는 것 자체가 결함 발견 이 된다.
왜 항공·의료에서는 여기까지 요구하는가
| 표준 | 산업 | 최고 등급에서 요구하는 커버리지 |
|---|---|---|
| DO-178C | 항공 (기내 소프트웨어) | Level A에서 MC/DC |
| ISO 26262 | 자동차 | ASIL D에서 MC/DC |
| IEC 62304 / FDA 지침 | 의료기기 | 위험 등급에 따라 분기 이상 |
| EN 50128 | 철도 신호 | SIL 4에서 MC/DC 권고 |
공통점은 하나다. 실패의 결과가 사람의 생명이고, 되돌릴 수 없으며, 핫픽스를 배포할 수 없다. 비행 중인 항공기에 앱스토어 업데이트를 밀어 넣을 수는 없다. 이런 영역에서는 "발견 못 한 결함 하나의 기대 손실"이 사실상 무한대에 가깝기 때문에, 검증 비용이 개발 비용의 몇 배가 되더라도 정당화된다. 인증기관에 증거를 제출해야 한다는 점도 크다 — MC/DC 리포트는 품질 활동이기 이전에 규제 문서다.
왜 일반 웹·앱 서비스는 여기까지 안 가는가
같은 계산을 일반 서비스에 해보면 답이 뒤집힌다.
| 축 | 안전 필수 시스템 | 일반 웹·앱 서비스 |
|---|---|---|
| 결함 1건의 최악 결과 | 인명 사고 | 사용자 이탈, 환불, 사과문 |
| 수정 배포까지 걸리는 시간 | 몇 개월(재인증 포함) | 몇 시간 |
| 요구사항 변경 주기 | 수년 | 수 주 |
| MC/DC 확보 비용 | 정당화됨 | 케이스 수보다 유지보수 비용 이 문제 |
마지막 줄이 실무에서 가장 결정적이다. MC/DC 케이스를 한 번 만드는 비용보다, 2주마다 로직이 바뀔 때 그 케이스들을 계속 고쳐 나가는 비용 이 훨씬 크다. 요구사항이 자주 바뀌는 제품에서 MC/DC를 강제하면, 팀은 곧 테스트를 고치는 데 지쳐 테스트를 지우기 시작한다. 그래서 일반 서비스에서는 분기 커버리지를 기본선으로 두고, 결제·정산·권한·개인정보처럼 틀렸을 때 비싼 모듈에만 조건/판정 수준을 요구하는 선택적 적용이 현실적인 답이다.
"어디까지 갈 것인가"는 기술 판단이 아니라 위험 판단 이라는 점을 기억하자. 이건 모듈 4(테스트 계획·리스크)에서 배울 위험 기반 테스트의 사고방식과 정확히 같다.
커버리지 100%가 결함 0%를 의미하지 않는 이유 — 코드로 보기
말로만 들으면 "그래도 100%면 어느 정도 안심 아닌가" 싶다. 직접 보자.
function getFinalPrice(price, discount) {
if (discount > 0) {
return price + discount; // 버그: 빼야 하는데 더하고 있다
}
return price;
}이 함수에 붙은 테스트다.
test('할인이 있으면 값이 계산된다', () => {
expect(getFinalPrice(10000, 1000)).toBeGreaterThan(0);
});
test('할인이 없으면 원래 가격', () => {
expect(getFinalPrice(10000, 0)).toBe(10000);
});두 테스트를 돌리면 구문 커버리지 100%, 분기 커버리지 100%, 조건 커버리지
100% 가 나온다. 리포트는 온통 초록색이다. 그런데 1,000원 할인 쿠폰을 쓰면
가격이 1,000원 오른다. 첫 테스트의 어서션이 toBeGreaterThan(0)이라 11,000도
통과시켰기 때문이다.
핵심은 이것이다. 커버리지는 "실행"을 재고, 결함 발견은 "검증"에서 나온다. 어서션이 약하면 커버리지는 100%인 채로 결함이 그대로 통과한다. 극단적으로는 어서션이 하나도 없는 테스트(그냥 함수를 호출만 하는 테스트)도 커버리지 100%를 만들 수 있다.
커버리지가 구조적으로 못 잡는 결함을 정리하면 이렇다.
| 결함 유형 | 예시 | 왜 커버리지에 안 걸리나 |
|---|---|---|
| 약한 어서션 | 위 예시 | 줄은 실행됐다 |
| 어서션 없음 | 호출만 하는 테스트 | 실행 카운터는 올라간다 |
| 누락된 코드 | 기획서엔 있는 "쿠폰 중복 사용 금지"가 구현 안 됨 | 없는 줄은 셀 수 없다 |
| 잘못된 경계 | > 를 써야 할 자리에 >= | 그 줄은 어느 쪽이든 실행된다 |
| 잘못된 상수 | 3만원 기준을 5만원으로 씀 | 조건문은 정상 실행된다 |
| 상태·순서 결함 | 두 번 연속 클릭, 뒤로가기 후 재제출 | 각 줄은 이미 다 실행됐다 |
| 동시성 결함 | 같은 재고를 동시에 주문 | 실행 순서는 커버리지가 안 본다 |
| 비기능 결함 | 응답이 8초 걸림, 권한 없이 접근됨 | 성능·보안은 측정 대상이 아니다 |
이 표에서 "누락된 코드" 줄이 가장 무섭다. 커버리지는 존재하는 코드에 대해서만 계산되므로, 구현되지 않은 요구사항은 분모에 아예 들어가지 않는다. "요구사항 커버리지"(모듈 2·4에서 다루는 추적성)와 코드 커버리지가 전혀 다른 숫자인 이유가 여기 있다.
정리하면 한 줄이다. 커버리지는 좋은 테스트의 필요조건이지 충분조건이 아니다. 커버리지가 낮으면 확실히 문제가 있고, 커버리지가 높다고 문제가 없는 건 아니다.
QA가 커버리지 리포트를 읽는 법
실무에서 QA가 커버리지를 마주치는 자리는 대개 정해져 있다. PR에 붙은 CI 코멘트,
또는 개발자가 공유한 리포트 링크 다. JavaScript는 Istanbul(nyc, Jest의
--coverage), Java는 JaCoCo, Python은 coverage.py가 대표적이고, 이 결과를
SonarQube가 모아 보여주는 구성이 흔하다.
Istanbul의 텍스트 리포트는 이렇게 생겼다.
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------|---------|----------|---------|---------|-------------------
All files | 82.4 | 61.2 | 88.0 | 82.1 |
cart.js | 95.0 | 90.0 | 100.0 | 95.0 | 42
payment.js | 48.3 | 22.5 | 50.0 | 47.9 | 15-31,55-70
utils/date.js | 100.0 | 100.0 | 100.0 | 100.0 |QA가 여기서 봐야 할 것은 맨 윗줄의 82.4가 아니다.
| 볼 것 | 왜 |
|---|---|
| 파일별 편차 | 총계 82.4는 date.js의 100%가 끌어올린 숫자다. 정작 payment.js가 48.3이다 |
| % Branch 열 | 분기가 구문보다 항상 낮다. payment.js는 22.5% — 예외·실패 경로가 거의 안 밟혔다는 뜻이다 |
| 두 열의 격차 | 구문 48.3 vs 분기 22.5처럼 벌어지면 "정상 경로만 테스트했다"는 신호다 |
| Uncovered Line #s | 진짜 정보는 여기다. 그 줄을 열어보면 대개 환불·실패·타임아웃 처리다 |
| 변경분 커버리지 | 이번 PR에서 새로 추가·수정된 줄이 커버됐는가. 전체 숫자보다 훨씬 유용하다 |
그리고 QA가 실제로 던질 질문은 이런 모양이 된다. 숫자를 지적하는 게 아니라 미커버 구간이 무엇인지를 묻는 것 이 요령이다.
| 리포트에서 본 것 | QA가 던질 질문 |
|---|---|
payment.js 15-31 미커버 | "15~31줄이 결제 실패 처리 같은데, 이 경로는 어떻게 검증하고 계신가요?" |
| 분기 22.5% | "예외 경로가 대부분 안 밟힌 것 같습니다. 제가 수동으로 커버할 목록을 만들까요?" |
| 총계는 82%인데 신규 파일이 0% | "이번에 추가된 모듈에 테스트가 아직 없는 것 같은데, 릴리즈 전에 붙일 계획인가요?" |
| 100%인 파일 | "이 파일은 100%인데, 어서션이 결과값까지 확인하는지 한 번 같이 볼 수 있을까요?" |
마지막 줄이 이 레슨의 앞 절과 이어진다. 100%라는 숫자를 보면 안심할 게 아니라, 그 100%가 어떤 어서션 위에 서 있는지를 봐야 한다.
"커버리지 80%"라는 숫자를 믿으면 안 되는 이유
팀 목표로 "커버리지 80%"를 거는 일이 흔하다. 이 숫자 하나만 보고 안심하면 안 되는 이유가 네 가지다.
- 분모가 조작 가능하다. 자동 생성 코드, 설정 파일, 타입 정의를 측정 대상에 포함하거나 제외하는 것만으로 퍼센트가 몇십 씩 움직인다. "무엇을 세지 않기로 했는가"가 숫자보다 중요하다.
- 쉬운 코드로 채운 80%가 있다. 날짜 포맷 유틸 같은 코드는 테스트가 쉬워서 금방 100%가 된다. 그 100%들이 총계를 끌어올리는 동안 결제 모듈은 40%에 머문다.
- 지표가 목표가 되면 왜곡된다. 커버리지를 KPI로 걸면 어서션 없는 테스트, 의미 없는 호출 테스트가 늘어난다. 모듈 2(레슨 2)에서 본 대리 지표의 함정 이 그대로 재현되는 사례다.
- 통합·E2E 테스트가 우연히 지나간 줄도 커버로 잡힌다. 로그인 한 번 하면 수십 개 파일이 실행되지만, 그게 그 코드를 "검증했다"는 뜻은 아니다.
그래서 실무에서 쓸 만한 기준은 총계 퍼센트가 아니라 이 세 가지다.
- 변경분 커버리지(diff coverage) — 이번에 바뀐 줄만 대상으로 본다. 기존 레거시의 낮은 커버리지에 발목 잡히지 않으면서, 새 코드의 품질은 지킬 수 있다.
- 모듈별 차등 기준 — 결제·인증·정산은 높게, 화면 표시 코드는 낮게.
- 커버리지 하락 금지 — 절대값보다 "이번 PR로 떨어지지 않았는가"가 관리하기 쉽고 논쟁도 적다.
도구
SonarQube는 구문·분기 커버리지 결과를 모아 추세와 임계값(Quality Gate)으로
보여주는 대중적인 도구다. 실제 측정은 언어별 도구(Istanbul, JaCoCo,
coverage.py)가 하고 SonarQube는 그 결과를 받아 표시하는 구조라는 점을
알아두면 리포트 출처를 헷갈리지 않는다.
VectorCAST, Test RealTime, LDRA 같은 도구는 MC/DC까지 측정하고 인증 제출용 증거 문서 를 생성할 수 있어 안전이 중요한 임베디드·항공·자동차 산업에서 주로 쓰인다. 일반 웹 서비스 팀에서 이 도구들을 볼 일은 거의 없지만, "왜 저런 도구가 따로 존재하는가"를 알면 앞서 본 비용 판단이 자연스럽게 이해된다.
실무에서 왜 필요한가
명세 기반 기법만으로는 "코드에 있지만 요구사항 문서엔 언급 안 된 경로" 를 놓칠 수 있다. 개발자가 방어적으로 짜둔 에러 핸들링 분기, 성능을 위해 넣은 캐시 분기, 과거 요구사항의 잔재로 남은 조건 — 이런 건 명세서 어디에도 안 적혀 있다. 구조 기반 기법은 이 골목들을 드러낸다. 반대로 구조 기반만 쓰면 "코드가 원래 요구사항을 제대로 구현했는가"는 못 본다. 이 모듈 레슨 2에서 강조했듯 두 갈래는 항상 함께 쓴다.
조금 더 구체적으로, 커버리지를 읽을 줄 아는 QA가 실무에서 만드는 차이는 네 가지다.
첫째, 테스트 공백의 위치를 지목할 수 있다. "테스트가 부족한 것 같아요"는
의견이지만, "payment.js 15~31줄, 결제 실패 처리 경로가 미커버입니다"는
사실이다. 사실로 쓰인 지적은 반박당하지 않는다 — 모듈 6(결함 리포트)에서 배운
원칙이 여기서도 그대로 통한다.
둘째, 수동 테스트의 우선순위가 잡힌다. 단위 테스트가 안 닿는 구간이 드러나면, 그 구간이 곧 수동·탐색적 테스트가 반드시 커버해야 할 목록 이 된다. 커버리지 리포트는 개발자만 보는 문서가 아니라 QA의 테스트 범위 설계 입력값이다.
셋째, 릴리즈 판단의 근거가 된다. "이번 릴리즈의 변경분 커버리지가 30%이고, 미커버 구간이 환불 로직입니다"는 릴리즈 회의에서 쓸 수 있는 근거다. 감이 아니라 구조적 사실이기 때문이다.
넷째, "커버리지 높이자"는 요구를 올바른 방향으로 돌릴 수 있다. 숫자만 올리는 방식과 위험한 경로부터 채우는 방식은 결과가 완전히 다르다. 이 차이를 설명할 수 있는 사람이 팀에 QA밖에 없는 경우가 많다.
채용시장에서의 위치
화이트박스 커버리지는 QA 면접에서 개념 확인용 단골 질문 이다. 자주 나오는 형태는 이렇다.
- "구문 커버리지와 분기 커버리지의 차이를 설명해보세요."
- "분기 커버리지 100%인데 조건 커버리지가 100%가 아닐 수 있나요? 반대는요?"
- "커버리지 100%면 버그가 없다고 볼 수 있나요?"
- "우리 팀 커버리지가 80%인데, QA로서 이 숫자를 어떻게 보시겠어요?"
두 번째 질문이 변별력이 높다. 이 레슨의 표 하나만 떠올리면 "조건 커버리지 100%인데 분기 커버리지가 50%인 경우가 실제로 있습니다" 라고 예를 들어 답할 수 있고, 이건 외운 정의를 말하는 지원자와 확실히 구분된다.
네 번째 질문에서 면접관이 기대하는 답은 "80%면 낮습니다/높습니다"가 아니다. "총계보다 파일별 편차와 분기 커버리지, 그리고 변경분 커버리지를 보겠습니다" 가 좋은 답이다. 숫자를 평가하는 사람이 아니라 숫자의 구조를 아는 사람 으로 보이기 때문이다.