[태그:] 분기 커버리지

  • 테스트, 얼마나 충분히 하셨나요? 코드 커버리지 너머의 이야기

    테스트, 얼마나 충분히 하셨나요? 코드 커버리지 너머의 이야기

    소프트웨어 개발 프로젝트가 막바지에 이르면 늘 빠지지 않고 등장하는 질문이 있습니다. “테스트는 충분히 했나요?”, “우리가 만든 제품, 이대로 출시해도 괜찮을까요?” 이때 이 질문에 대한 막연한 감이나 느낌이 아닌, 객관적인 데이터로 답할 수 있게 해주는 핵심 지표가 바로 ‘테스트 커버리지(Test Coverage)’입니다. 테스트 커버리지는 우리가 준비한 테스트 케이스가 테스트 대상의 특정 부분을 얼마나 많이 검증했는지를 정량적인 수치(%)로 나타낸 것입니다. 이는 우리가 얼마나 꼼꼼하게 테스트했는지를 보여주는 일종의 ‘건강검진 결과표’와 같습니다.

    하지만 많은 사람들이 테스트 커버리지를 단순히 ‘코드 커버리지’와 동일시하는 오해를 하곤 합니다. 코드의 몇 줄이나 실행되었는지를 측정하는 코드 커버리지는 매우 중요하지만, 그것이 테스트의 전체를 대변하지는 않습니다. 진정한 의미의 품질을 확보하기 위해서는 사용자의 요구사항 관점에서의 ‘기능 커버리지’와 코드의 내부 구조 관점에서의 ‘코드 커버리지’를 모두 균형 있게 바라보는 시각이 필요합니다.

    본 글에서는 테스트 커버리지의 두 가지 큰 축인 기능 커버리와 코드 커버리(라인 커버리 포함)에 대해 각각의 개념과 측정 방법, 그리고 실제 프로젝트에서 어떻게 활용되는지를 깊이 있게 파헤쳐 보고자 합니다. 이 글을 통해 여러분은 100%라는 숫자의 함정에 빠지지 않고, 테스트 커버리지를 현명하게 해석하고 활용하여 소프트웨어의 품질을 실질적으로 향상시키는 방법을 배우게 될 것입니다.


    기능 커버리지 (Functional Coverage)

    핵심 개념: 사용자의 요구사항을 얼마나 테스트했는가?

    기능 커버리지는 ‘블랙박스 테스트’의 관점에서, 시스템이 수행해야 할 모든 기능적 요구사항들이 테스트에 의해 얼마나 검증되었는지를 측정하는 지표입니다. 즉, 소스 코드가 어떻게 작성되었는지에 관계없이, 순전히 ‘사용자에게 제공하기로 약속한 기능’의 목록을 기준으로 테스트의 충분성을 평가하는 것입니다. 이는 “우리가 만들어야 할 올바른 제품(Right Product)을 제대로 테스트하고 있는가?”라는 근본적인 질문에 답하는 과정입니다.

    기능 커버리지의 측정 기준은 보통 요구사항 명세서, 유스케이스, 사용자 스토리(User Story), 기능 목록(Feature List) 등이 됩니다. 예를 들어, 총 100개의 요구사항 중 90개에 대한 테스트 케이스를 설계하고 수행했다면, 기능 커버리지는 90%가 됩니다. 높은 기능 커버리지는 우리가 제품의 중요한 기능들을 빠뜨리지 않고 검증하고 있다는 강력한 증거가 됩니다.

    기능 커버리지는 다음과 같은 질문에 답을 줍니다.

    • 우리가 정의한 모든 비즈니스 규칙(Business Rule)이 테스트되었는가?
    • 모든 유스케이스의 정상 시나리오와 예외 시나리오가 검증되었는가?
    • 사용자 스토리의 모든 인수 조건(Acceptance Criteria)을 만족하는 테스트가 존재하는가?
    • 메뉴의 모든 항목, 화면의 모든 버튼에 대한 테스트가 이루어졌는가?

    이처럼 기능 커버리지는 개발팀이 아닌 기획자, 현업 사용자, 고객의 관점에서 테스트의 진행 상황과 범위를 가장 직관적으로 이해할 수 있게 해주는 중요한 소통의 도구가 됩니다.

    측정 방법 및 사례: 요구사항 추적 매트릭스(RTM) 활용하기

    기능 커버리지를 체계적으로 관리하고 측정하는 데 가장 효과적인 도구는 ‘요구사항 추적 매트릭스(Requirement Traceability Matrix, RTM)’입니다. RTM은 요구사항, 테스트 케이스, 그리고 발견된 결함 간의 관계를 매핑하여 추적할 수 있도록 만든 표입니다.

    한 온라인 쇼핑몰의 회원가입 기능에 대한 요구사항과 테스트 케이스를 RTM으로 관리하는 예시를 살펴보겠습니다.

    요구사항 목록

    • REQ-001: 사용자는 아이디, 비밀번호, 이메일, 이름을 입력하여 회원가입을 할 수 있어야 한다.
    • REQ-002: 아이디는 6자 이상 12자 이하의 영문/숫자 조합이어야 한다.
    • REQ-003: 비밀번호는 8자 이상이며, 특수문자를 1개 이상 포함해야 한다.
    • REQ-004: 이미 존재하는 아이디로는 가입할 수 없다.

    요구사항 추적 매트릭스 (RTM)

    요구사항 ID요구사항 내용테스트 케이스 ID테스트 케이스 상태관련 결함 ID
    REQ-001기본 정보 입력 가입TC-JOIN-001Pass
    REQ-002아이디 유효성 검증TC-JOIN-002 (정상)Pass
    TC-JOIN-003 (5자)Pass
    TC-JOIN-004 (한글)Pass
    REQ-003비밀번호 유효성 검증TC-JOIN-005 (정상)Pass
    TC-JOIN-006 (7자)FailDEF-501
    REQ-004아이디 중복 검증TC-JOIN-007Pass

    이 RTM을 통해 우리는 다음과 같은 사실을 명확히 알 수 있습니다.

    • 총 4개의 요구사항이 존재하며, 모든 요구사항에 대해 최소 1개 이상의 테스트 케이스가 매핑되어 있다. 따라서 이 범위 내에서 기능 커버리지는 100%라고 말할 수 있다.
    • REQ-003(비밀번호 유효성 검증)을 테스트하는 과정에서 TC-JOIN-006이 실패했고, 관련 결함(DEF-501)이 등록되었다. 이는 해당 기능이 아직 불안정하다는 것을 의미한다.
    • 만약 특정 요구사항에 매핑된 테스트 케이스가 아예 없다면, 해당 기능은 전혀 테스트되지 않고 있다는 위험 신호이며, 즉시 테스트 케이스를 보강해야 한다.

    최근 애자일 개발 환경에서는 Jira와 같은 도구를 사용하여 사용자 스토리(요구사항)와 테스트 케이스, 버그를 직접 연결(linking)하여 RTM을 자동으로 생성하고 관리합니다. 이를 통해 제품 책임자(PO)나 프로젝트 관리자는 언제든지 실시간으로 기능별 테스트 진행 현황과 품질 수준을 파악하고, 릴리스 여부를 데이터에 기반하여 결정할 수 있습니다.


    코드 커버리지 (Code Coverage)

    핵심 개념: 우리의 코드가 얼마나 실행되었는가?

    코드 커버리지는 ‘화이트박스 테스트’의 관점에서, 테스트를 수행하는 동안 소프트웨어의 소스 코드가 얼마나 실행되었는지를 측정하는 지표입니다. 이는 “우리가 작성한 코드를 얼마나 촘촘하게 테스트하고 있는가?”라는 질문에 답하는 과정이며, 주로 개발자가 수행하는 단위 테스트(Unit Test)나 통합 테스트 단계에서 코드의 품질을 정량적으로 평가하기 위해 사용됩니다.

    높은 코드 커버리지는 테스트되지 않은 코드가 거의 없음을 의미하며, 이는 코드 내에 숨어 있을지 모를 잠재적인 결함을 발견할 가능성을 높여줍니다. 반대로 코드 커버리지가 낮다는 것은, 한 번도 실행되지 않은 코드가 많다는 뜻이며, 그 부분에 버그가 숨어 있어도 테스트 과정에서는 절대로 발견할 수 없음을 의미하는 명백한 위험 신호입니다.

    코드 커버리지는 측정 기준에 따라 여러 종류로 나뉘며, 가장 대표적인 것은 다음과 같습니다.

    • 구문 (Statement / Line) 커버리지: 코드의 모든 실행문이 최소 한 번 이상 실행되었는지를 측정합니다.
    • 분기 (Branch / Decision) 커버리지: ‘if’, ‘switch’, ‘while’과 같은 조건문의 결과가 참(True)인 경우와 거짓(False)인 경우를 모두 한 번 이상 실행했는지를 측정합니다.
    • 경로 (Path) 커버리지: 프로그램 내에서 실행될 수 있는 모든 가능한 경로를 테스트했는지를 측정합니다. 이론적으로 가장 강력하지만, 경로의 수가 기하급수적으로 많아져 현실적으로 100% 달성은 거의 불가능합니다.

    이 중에서 가장 기본적이면서 널리 사용되는 것이 바로 라인 커버리지와 분기 커버리지입니다.

    라인 커버리지 (Line Coverage) / 구문 커버리지 (Statement Coverage)

    라인 커버리지는 코드 커버리지 중에서 가장 이해하기 쉽고 기본적인 척도입니다. 전체 실행 가능한 소스 코드 라인(Line) 중에서 테스트 중에 한 번 이상 실행된 라인의 비율을 나타냅니다.

    라인 커버리지(%) = (실행된 라인 수 / 전체 실행 가능 라인 수) * 100

    예를 들어, 다음과 같은 간단한 자바(Java) 코드가 있다고 가정해 봅시다.

    Java

    public int calculateBonus(int performanceGrade, int salary) {
    int bonus = 0; // Line 1
    if (performanceGrade == 1) { // Line 2
    bonus = salary * 0.2; // Line 3
    } else {
    bonus = salary * 0.1; // Line 4
    }
    System.out.println("보너스 계산 완료"); // Line 5
    return bonus; // Line 6
    }

    이 함수를 테스트하기 위해 다음과 같은 테스트 케이스를 하나 실행했습니다.

    • TC_001:calculateBonus(1, 1000)

    이 테스트 케이스를 실행하면 코드는 1, 2, 3, 5, 6번 라인을 실행하게 됩니다. 4번 라인(else 블록)은 실행되지 않습니다. 이 함수의 전체 실행 가능 라인 수는 6개이고, 그중 5개가 실행되었으므로 라인 커버리지는 (5 / 6) * 100 = 약 83.3%가 됩니다.

    라인 커버리지 100%를 달성하기 위해서는 4번 라인을 실행시키는 테스트 케이스, 즉 performanceGrade가 1이 아닌 경우(예: calculateBonus(2, 1000))를 추가해야 합니다.

    분기 커버리지 (Branch Coverage) / 결정 커버리지 (Decision Coverage)

    라인 커버리지만으로는 충분하지 않은 경우가 있습니다. 분기 커버리지는 코드 내 모든 분기문(조건문)의 가능한 결과(참/거짓)가 최소 한 번 이상 테스트되었는지를 측정합니다. 이는 라인 커버리지보다 더 강력하고 신뢰성 있는 척도로 여겨집니다.

    분기 커버리지(%) = (실행된 분기 수 / 전체 분기 수) * 100

    위의 calculateBonus 함수 예시에서 if (performanceGrade == 1) 라는 조건문에는 ‘참(True)’인 경우와 ‘거짓(False)’인 경우, 이렇게 2개의 분기가 존재합니다.

    • TC_001 (calculateBonus(1, 1000)) 을 실행하면 ‘참’ 분기만 테스트됩니다. 이 경우 분기 커버리지는 (1 / 2) * 100 = 50%가 됩니다. (라인 커버리지는 83.3%였지만 분기 커버리지는 더 낮습니다.)
    • 분기 커버리지 100%를 달성하기 위해서는, ‘거짓’ 분기를 실행시키는 TC_002 (calculateBonus(2, 1000)) 를 반드시 추가해야 합니다.

    이처럼 분기 커버리지는 조건문의 논리적 오류를 찾아내는 데 라인 커버리지보다 훨씬 효과적입니다. 최근에는 많은 개발팀이 최소한의 품질 기준으로 ‘분기 커버리지 80% 이상’과 같은 목표를 설정하고, CI/CD(지속적 통합/지속적 배포) 파이프라인에 코드 커버리지 측정 도구(JaCoCo, Cobertura, Istanbul 등)를 연동합니다. 개발자가 코드를 제출할 때마다 자동으로 단위 테스트와 함께 커버리지를 측정하고, 목표치에 미달하면 빌드를 실패시켜 코드 품질을 강제하는 방식을 널리 사용하고 있습니다.


    마무리: 100% 커버리지의 함정과 현명한 활용법

    테스트 커버리지는 테스트의 충분성을 평가하는 매우 유용한 지표임이 틀림없습니다. 하지만 커버리지 숫자에만 맹목적으로 집착하는 것은 위험하며, 이를 ‘100% 커버리지의 함정’이라고 부릅니다.

    • 100% 코드 커버리지가 완벽한 품질을 보장하지 않는다: 코드 커버리지 100%는 모든 코드 라인이나 분기가 ‘실행’되었다는 사실만을 알려줄 뿐, 그 실행 결과가 ‘올바른지’를 보장하지는 않습니다. 테스트 케이스의 단언문(Assertion)이 부실하다면, 코드는 실행되지만 잠재적인 버그는 그대로 통과될 수 있습니다. 또한, 코드에는 없지만 요구사항에 누락된 기능(Missing Feature)은 코드 커버리지로는 절대 찾아낼 수 없습니다.
    • 기능 커버리지의 맹점: 기능 커버리지가 100%라 할지라도, 이는 우리가 정의한 요구사항을 모두 테스트했다는 의미일 뿐, 그 요구사항 자체가 잘못되었거나 불완전할 가능성을 배제하지 못합니다. 또한, 특정 기능의 비정상적인 입력값이나 경계값에 대한 테스트가 부실할 수도 있습니다.
    • 비용과 효용의 문제: 코드 커버리지를 80%에서 90%로 올리는 것보다, 99%에서 100%로 올리는 데는 훨씬 더 많은 노력이 필요합니다. 거의 발생하지 않는 예외적인 경로까지 모두 테스트하기 위해 막대한 비용을 들이는 것이 항상 효율적인 것은 아닙니다.

    결론적으로, 현명한 테스트 전략은 기능 커버리지와 코드 커버리지를 상호 보완적으로 사용하는 것입니다. 먼저, 기능 커버리지를 통해 우리가 비즈니스적으로 중요한 모든 기능을 빠짐없이 테스트하고 있는지 큰 그림을 확인해야 합니다. 그 다음, 코드 커버리지를 사용하여 우리가 작성한 코드 중 테스트되지 않은 사각지대는 없는지, 특히 복잡한 로직을 가진 중요한 모듈의 내부를 얼마나 깊이 있게 검증했는지 세부적으로 점검해야 합니다.

    테스트 커버리지는 품질의 최종 목표가 아니라, 우리가 어디에 더 집중해야 하는지 알려주는 ‘내비게이션’입니다. 이 지표를 현명하게 해석하고, 리스크 기반의 테스트 전략과 결합하여 사용할 때, 비로소 우리는 한정된 자원 속에서 소프트웨어의 품질을 효과적으로 높일 수 있을 것입니다.