테스트 커버리지 — JaCoCo로 측정하고 해석하기

커버리지는 테스트가 코드를 얼마나 실행했는지 보여 주는 지표입니다. JaCoCo의 계측 원리, 라인·브랜치 커버리지의 차이, 빌드 게이트 설정, 그리고 커버리지 숫자를 맹신하면 안 되는 이유까지 정리합니다.

· 7 min read · PALDYN Team

지난 글에서 아키텍처 규칙을 테스트로 강제하는 법을 살펴봤습니다. 테스트를 열심히 작성하다 보면 자연스레 떠오르는 질문이 있습니다. “우리 테스트는 코드를 얼마나 검증하고 있을까?” 이 물음에 숫자로 답하는 것이 테스트 커버리지입니다. 자바 진영에서 사실상 표준 도구는 JaCoCo(Java Code Coverage)입니다. 이번 글에서는 JaCoCo가 어떻게 커버리지를 재는지, 그 숫자를 어떻게 읽고 어디까지 믿어야 하는지 정리합니다.

JaCoCo는 어떻게 측정하는가

JaCoCo 측정 흐름 — 계측, 실행 기록, 리포트

JaCoCo는 바이트코드 계측(instrumentation) 방식으로 동작합니다. 자바 에이전트가 클래스 로딩 시점에 바이트코드에 작은 “프로브”를 삽입하고, 테스트가 실행되며 어떤 프로브가 지나갔는지 jacoco.exec 파일에 기록합니다. 테스트가 끝나면 이 실행 기록과 원본 클래스를 대조해 어떤 라인·분기가 실행됐는지 계산하고, HTML이나 XML 리포트로 출력합니다. 소스 수정 없이 바이트코드만 건드리므로, 기존 테스트를 그대로 두고 커버리지를 잴 수 있습니다.

커버리지의 여러 종류

커버리지 종류 — 라인, 브랜치, 메서드

가장 흔히 보는 숫자는 라인 커버리지지만, 사실 이것은 가장 느슨한 지표입니다. 분기가 있는 코드를 생각해 봅시다.

int classify(int score) {
    if (score >= 60) {
        return PASS;
    }
    return FAIL;
}

classify(80) 하나만 테스트하면 if 라인과 return PASS를 실행하므로 라인 커버리지는 높게 나오지만, score < 60인 경우는 한 번도 검사하지 않았습니다. 브랜치 커버리지if의 참·거짓 양쪽이 모두 실행됐는지를 보기 때문에, 이런 누락을 잡아냅니다. 그래서 라인 커버리지보다 브랜치 커버리지를 더 신뢰할 만한 지표로 봅니다. 메서드·클래스 커버리지는 “아예 한 번도 호출되지 않은 코드”라는 거시적 사각지대를 찾는 데 유용합니다.

Gradle 설정과 빌드 게이트

JaCoCo 플러그인을 적용하고 리포트 태스크를 테스트에 연결합니다.

plugins {
    id 'jacoco'
}

jacocoTestReport {
    dependsOn test
    reports {
        html.required = true
        xml.required = true
    }
}

숫자를 측정하는 데 그치지 않고, 일정 기준에 미달하면 빌드를 실패시키는 게이트를 걸 수 있습니다.

jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                counter = 'BRANCH'
                minimum = 0.70
            }
        }
    }
}

check.dependsOn jacocoTestCoverageVerification

이렇게 하면 브랜치 커버리지가 70% 아래로 떨어질 때 check(따라서 CI)가 실패합니다. 새 코드가 테스트 없이 슬며시 들어오는 것을 막는 안전장치입니다.

숫자를 맹신하지 말 것

커버리지는 강력한 도구지만, 목표가 아니라 신호입니다. 100% 커버리지가 버그 없음을 보장하지는 않습니다. 다음 테스트를 봅시다.

@Test
void classify_실행만_한다() {
    classify(80);
    classify(50);   // 단언이 하나도 없다
}

이 테스트는 두 분기를 모두 실행해 브랜치 커버리지를 100%로 만들지만, 아무것도 검증하지 않습니다. 커버리지는 “어떤 코드가 실행됐는가”만 셀 뿐, “결과가 옳은지 단언했는가”는 보지 못합니다. 그래서 높은 커버리지를 강제하면, 단언 없이 숫자만 채우는 가짜 테스트가 늘어나는 부작용이 생기기도 합니다.

커버리지는 “어디가 전혀 테스트되지 않았는가”를 찾는 데 쓰는 것이 가장 건강합니다. 0%인 핵심 도메인 클래스를 발견하는 데는 유용하지만, 95%를 99%로 끌어올리는 데 집착하는 것은 대개 헛수고입니다. 이 “실행은 하지만 검증은 안 하는” 약점을 정면으로 공격하는 기법이 바로 다음 글의 뮤테이션 테스트입니다.

정리

JaCoCo는 바이트코드 계측으로 테스트가 실행한 라인과 분기를 기록해 커버리지를 측정합니다. 라인 커버리지보다 브랜치 커버리지가 더 의미 있는 지표이며, 빌드 게이트로 최소 기준을 강제해 회귀를 막을 수 있습니다. 다만 커버리지는 실행 여부만 셀 뿐 단언의 품질은 보지 못하므로, 숫자 자체를 목표로 삼기보다 사각지대를 찾는 신호로 활용하는 것이 옳습니다. 다음 글에서는 이 한계를 보완하는 뮤테이션 테스트를 다룹니다.


지난 글: ArchUnit — 아키텍처를 테스트로 강제하기

다음 글: 뮤테이션 테스트 — PIT로 테스트의 품질을 검증하기


읽어주셔서 감사합니다. 😊