문장 커버리지로 테스트 품질 게이트 설계하기
문장 커버리지의 산식과 계측 방식, 품질 게이트 운영 기준, Branch·Mutation 지표와의 보완 관계를 정리한다.
2026-08-14 · 최초 발행 2025-12-22
실행된 문장을 확인하는 가장 낮은 단위의 커버리지
문장 커버리지는 화이트박스 테스트에서 실행 가능한 코드 문장 가운데 테스트가 실제로 통과한 문장의 비율을 보는 지표다. 단위 테스트의 빈 곳을 찾고, CI에서 품질 게이트를 운영하며, 리팩터링 전후의 안전망을 확인할 때 출발점이 된다.
산식은 다음과 같다.
Statement Coverage(%) = 실행된 문장 수 / 전체 실행 가능한 문장 수 × 100
공백, 주석, 선언만 있는 라인처럼 실행되지 않는 요소는 대상에서 제외한다. 다만 문장 단위의 정의는 언어와 도구에 따라 다르다. Python은 바이트코드 기준을 사용할 수 있고, JVM은 바이트코드와 라인 매핑을 기준으로 하며, JS는 소스맵 기반 계측을 사용할 수 있다.
대입문, 함수 호출, 제어문 본문, 예외 발생 지점, return은 문장에 포함된다. 반대로 import나 using 선언, 타입 선언, 주석과 공백은 포함하지 않는다. 실행 가능한 문장이 10개이고 그중 8개가 테스트에서 수행됐다면 결과는 80%다.
이 지표가 100%여도 모든 조건을 검증했다는 뜻은 아니다. if 내부 문장만 한 번 실행해도 그 문장은 커버된 것으로 집계되며, 조건의 반대 경로는 여전히 검증되지 않았을 수 있다.
계측과 보고서가 만드는 관측 지점
커버리지는 소스 계측(프리프로세싱) 또는 바이트코드 계측(런타임·빌드 타임)으로 수집한다. 계측 밀도와 보고서 형식에 비례해 실행 시간과 메모리가 늘어나는 경향이 있다.
보고서는 텍스트, HTML, XML, LCOV, JSON 형식으로 만들 수 있다. 이를 PR 코멘트, 품질 대시보드, 저커버리지 핫스팟 탐지에 연결하면 단순한 비율보다 변경과 위험의 위치를 더 잘 드러낼 수 있다.
Python에서는 coverage.py, pytest-cov를 사용할 수 있고 JVM 환경에서는 JaCoCo, IntelliJ 커버리지, SonarQube 통합이 가능하다. JS/TS에서는 Istanbul(nyc)과 V8 coverage가 해당 역할을 맡는다.
전체 기준과 변경 라인 기준을 분리한다
임계치를 하나의 숫자로만 운영하면 기존 저커버리지 코드가 품질 게이트의 부담으로 남는다. 전체 프로젝트에는 70~80%를 기본선으로 두고, 신규 또는 변경 코드에는 90% 이상을 권장하는 방식이 그 부담을 나누는 방법이다.
실무에서는 전체 80%, 변경 라인 90~95%처럼 별도의 기준을 둘 수 있다. 이렇게 하면 레거시 저커버리지 영역이 새 변경을 막는 문제를 줄이면서 신규 결함 유입을 억제할 수 있다.
리팩터링에서는 변경 전 커버리지 스냅샷을 남기고, 테스트를 보강한 뒤 코드를 바꾸고, 결과를 다시 비교한다. 마이크로서비스 CI 파이프라인이라면 빌드, 테스트와 계측, 리포트, 품질 게이트, 배포 순서로 연결할 수 있다. JaCoCo 또는 coverage.py, SonarQube, PR 코멘트 봇을 함께 쓰는 구성이 여기에 해당한다.
결함이 발생했을 때는 결함 재현 라인과 미커버 라인의 교집합을 찾아볼 수 있다. 미커버 상태인 고위험 라인부터 테스트를 추가하면 분석 우선순위를 잡기 쉽다.
수집부터 재시도까지의 흐름
측정을 시작하기 전에 소스 코드, 테스트 스위트, 커버리지 도구 설정을 준비한다. 생성 코드와 마이그레이션 스크립트처럼 제외할 대상도 먼저 정한다.
계측을 적용한 뒤 테스트를 실행하고, 트레이스를 수집·병합해 라인과 문장에 매핑한다. 이후 HTML이나 LCOV 등의 리포트를 만들고 품질 게이트를 판정한다. 실패했다면 로그를 확인해 제외 규칙 또는 테스트를 보완한 뒤 다시 실행한다.
런타임, 컴파일러, 플러그인 사이의 버전 불일치는 먼저 호환성을 점검한다. CI 컨테이너의 권한이나 워크스페이스 경로·캐시도 측정 실패의 원인이 될 수 있다. 생성물과 빌드 산출물을 커버리지 대상에서 빼지 않으면 결과가 왜곡될 수 있다.
문장 실행률이 보여 주는 범위와 빈틈
| 지표 | 측정 단위 | 논리 보장 수준 | 성능 오버헤드 | 적용 용이성 | 권장 사용 영역 |
|---|---|---|---|---|---|
| Statement | 실행 가능한 문장 | 낮음 | 낮음 | 높음 | 기본선, 신규코드 게이트 |
| Branch | 분기(참/거짓) | 중간 | 중간 | 중간 | 조건/예외 흐름 검증 |
| Condition | 논리식의 각 원자 조건 | 높음 | 중간~높음 | 중간 | 복잡 조건 로직 |
| MC/DC | 조건 독립 영향 | 매우 높음 | 높음 | 낮음 | 안전/미션 크리티컬 |
| Path | 경로 전체 | 이론적 최고 | 매우 높음 | 낮음 | 제한적(작은 유닛) |
문장 커버리지는 기본선을 세우기 쉽지만, 논리 경로나 예외·에러 경로가 실제로 검증됐는지는 판단하지 못한다. 복잡한 조건을 다루는 코드에는 Branch, Condition, MC/DC를 함께 보며, 테스트가 실제 동작을 검증하는지 확인하려면 Mutation Testing을 병행할 수 있다.
운영에서 피할 수 없는 비용과 관리 항목
문장 커버리지를 도입하면 미커버 코드 비율을 가시화하고 목표치에 따른 개선 사이클을 만들 수 있다. 다만 도구, 프로젝트 크기, 리포트 형식에 따라 빌드 시간 오버헤드는 5~30% 범위에서 발생할 수 있다. 품질 게이트 차단 이벤트를 기준으로 결함 유입 차단 건수의 증가도 추적할 수 있다.
테스트 우선 문화가 자리 잡고 변경 영향 범위를 파악하기 쉬워지며, 리뷰는 저커버리지 핫스팟에 집중할 수 있다. 분기와 예외 경로를 보강하는 위험 기반 테스트 설계도 촉진된다.
운영 규칙은 전체 기준과 변경 라인 기준을 분리하는 데서 시작한다. 생성 코드, 마이그레이션 스크립트, 외부 SDK는 제외 목록으로 관리하고, 중복 로직과 예외 경로는 별도 테스트 케이스로 확보한다. 리포트 포맷은 LCOV 또는 XML로 통일하고 CI 아티팩트로 보존한다.
반대로 커버리지 수치 자체를 목표로 삼으면 동작을 검증하지 않는 테스트가 늘어날 수 있다. 큰 리포트 생성은 I/O 병목과 속도·비용 증가를 만들 수 있으며, 문장 커버리지만으로는 분기와 조건의 품질을 보장할 수 없다.
Python에서 coverage.py와 pytest를 연결하는 방법
Python 3.11, pytest 7.x, coverage.py 7.x를 전제로 한 예시다.
소스: src/calc.py
def div(a, b):
if b == 0:
raise ValueError("zero")
return a / b
def add(a, b):
return a + b
테스트: tests/test_calc.py
from src.calc import div, add
import pytest
def test_add():
assert add(2, 3) == 5
def test_div_normal():
assert div(6, 3) == 2
def test_div_zero():
with pytest.raises(ValueError):
div(1, 0)
실행은 다음과 같다.
pip install pytest coverage
coverage run -m pytest
coverage report -m
coverage html # htmlcov/index.html 확인
-m 옵션은 실행 가능한 문장을 기준으로 수집한다. 특정 라인은 # pragma: no cover 주석으로 제외할 수 있다.
Gradle과 JaCoCo로 JVM 리포트를 만드는 방법
Java 17, Gradle 8.x, JaCoCo 0.8.x 환경의 설정 예시다.
build.gradle.kts
plugins {
java
jacoco
}
tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
reports {
xml.required.set(true)
html.required.set(true)
}
}
실행 명령은 다음과 같다.
./gradlew test jacocoTestReport
# build/reports/jacoco/test/html/index.html 확인
정책에는 변경 라인 90%, 모듈 전체 80%의 커버리지 임계치를 둘 수 있다. generated/**, **/dto/**, **/config/**는 제외 대상으로 설정할 수 있다.