테스트 QA

높은 코드 커버리지에도 버그가 쏟아진다면? 찐 개발자를 위한 테스트 커버리지 실전 개선 가이드

강코의 코딩 일기 2026. 7. 24. 16:02
반응형

코드 커버리지 맹신은 위험합니다. 겉보기엔 높은 수치 뒤에 숨겨진 진짜 테스트 품질 문제를 파악하고, 브랜치, 조건, 돌연변이 커버리지를 활용해 견고한 소프트웨어를 만드는 실전 체크리스트를 공개합니다.

안녕하세요, 오랜만에 인사드립니다. 개발자로서 시스템의 견고함을 지키는 일, 늘 고민 많으시죠? 특히 테스트 커버리지는 개발팀의 건강 상태를 가늠하는 중요한 지표 중 하나로 여겨지곤 합니다. 그런데 말이죠, 혹시 이런 경험 없으신가요?

“우리 팀은 코드 커버리지 90% 이상을 유지하고 있는데, 왜 이렇게 버그가 많이 나올까요?”

네, 맞습니다. 겉보기엔 높은 커버리지 수치에 안도했지만, 막상 프로덕션 환경에서 예상치 못한 버그들이 터져 나와 당황했던 경험, 저뿐만은 아닐 겁니다. 라인 커버리지만으로는 결코 알 수 없는 테스트의 '진짜' 구멍들, 오늘은 그 허점들을 파악하고 브랜치, 조건, 돌연변이 커버리지를 활용해 견고한 테스트 전략을 세우는 실전 가이드를 함께 이야기해보려고 합니다. 단순한 이론이 아니라, 실제 프로젝트에 적용하며 겪었던 문제 해결 과정을 통해 시니어 개발자분들께 실질적인 도움이 될 만한 팁들을 공유해 드릴게요.

코드 커버리지 맹신을 피하는 테스트 커버리지 측정 및 개선 실전 체크리스트: 브랜치, 조건, 돌연변이 커버리지 활용 - covid, testing, corona test, covid-19, corona, coronavirus, sars-cov-2, concept, quick test, pcr, pcr-test, covid test, covid, covid, covid, covid, covid, corona, corona, covid test, covid test, covid test

Image by analogicus on Pixabay

문제 발생: 90% 코드 커버리지의 배신

"이 정도면 충분하지 않나요?" 착각의 시작

저희 팀이 개발하던 핵심 결제 모듈에서 있었던 일입니다. QA 단계에서는 발견되지 않았고, 릴리즈 후 특정 상황에서만 결제가 실패하는 치명적인 버그가 발생했죠. 문제는 해당 모듈의 라인 커버리지가 무려 95%에 달했다는 겁니다. 모든 개발자가 "이 정도면 웬만한 로직은 다 커버된 거 아니야?"라고 생각했거든요. 하지만 버그는 여지없이 터졌고, 이는 우리 팀의 테스트 전략에 근본적인 질문을 던지게 만들었습니다.

어떤 상황이었냐면요. VIP 고객이 특정 할인 쿠폰을 사용하는 경우에만 발생하는 버그였어요. 일반 고객 할인, VIP 고객 일반 결제, 일반 고객 쿠폰 결제 등 기본적인 시나리오는 모두 테스트 커버리지를 통과했는데, 두 가지 조건(VIP 여부와 특정 쿠폰 사용 여부)이 동시에 만족될 때의 엣지 케이스를 놓쳤던 거죠. 95%라는 수치가 무색할 만큼, 가장 중요한 비즈니스 로직의 핵심 분기가 전혀 테스트되지 않았던 겁니다.

원인 분석: 라인 커버리지가 숨기는 진짜 허점들

라인 커버리지의 한계, 그리고 그 너머

라인 커버리지는 말 그대로 코드의 '라인'이 실행되었는지 여부만 측정합니다. 즉, if (conditionA && conditionB)와 같은 복합 조건문이 있을 때, conditionA && conditionB가 한 번이라도 true가 되어 if 블록이 실행되면 해당 라인은 커버된 것으로 간주되죠. 하지만 conditionA가 false이고 conditionB가 true인 경우, 혹은 그 반대의 경우는 전혀 확인하지 못합니다. 바로 여기서 문제가 발생합니다.

저희 팀의 결제 모듈 버그도 마찬가지였어요. VIP 여부와 특정 쿠폰 사용 여부를 동시에 체크하는 로직이 있었는데, VIP가 아니거나 특정 쿠폰을 사용하지 않은 일반적인 케이스만 테스트 코드로 커버되어 있었던 거죠. VIP이면서 동시에 특정 쿠폰을 사용한 "진짜" 핵심 케이스는 테스트되지 않았는데도 라인 커버리지는 높게 나왔던 겁니다.

이런 허점들을 보완하기 위해 우리는 다음과 같은 고급 테스트 커버리지 지표들에 주목해야 합니다.

  1. 브랜치 커버리지 (Branch Coverage): 조건문의 모든 분기 경로를 테스트했는지 확인합니다. 예를 들어, if-else 문이 있다면 if 블록과 else 블록이 모두 실행되었는지 측정하는 거죠.
  2. 조건 커버리지 (Condition Coverage): 복합 조건문 (예: A && B, A || B) 내의 각 개별 조건 (A, B)이 true와 false 값을 모두 가지도록 테스트했는지 확인합니다. 이는 브랜치 커버리지보다 더 세분화된 측정 기준입니다.
  3. 돌연변이 커버리지 (Mutation Coverage): 코드에 의도적으로 작은 변경(돌연변이)을 가하고, 기존 테스트 스위트가 이 변경된 코드를 실패시키는지 확인합니다. 만약 돌연변이가 발생했는데도 테스트가 모두 통과한다면, 해당 테스트는 변경된 코드를 감지하지 못했다는 의미이며, 테스트의 품질이 낮다고 판단할 수 있습니다. 이는 테스트 코드 자체의 견고성을 측정하는 강력한 방법이죠.

이해를 돕기 위해 각 커버리지 유형을 비교하는 테이블을 준비했습니다.

커버리지 유형 측정 대상 측정 목표 장점 단점 / 고려사항
라인 커버리지 소스 코드의 각 실행 가능한 라인 모든 라인이 한 번 이상 실행되었는지 가장 기본적이고 이해하기 쉬움, 쉽게 측정 가능 조건문, 분기 로직의 모든 경우를 커버하지 못함
브랜치 커버리지 조건문(if, for, while 등)의 모든 분기 모든 분기(true/false)가 한 번 이상 실행되었는지 라인 커버리지보다 심층적으로 제어 흐름 테스트 복합 조건문 내 개별 조건까지는 확인 불가
조건 커버리지 복합 조건문 내 각 개별 조건 각 개별 조건이 true/false 값을 모두 가졌는지 복합 조건문의 모든 논리적 조합을 파악하는 데 도움 테스트 케이스 수가 급격히 증가할 수 있음
돌연변이 커버리지 소스 코드에 생성된 돌연변이 (작은 코드 변경) 테스트 스위트가 모든 돌연변이를 실패시키는지 테스트 코드 자체의 품질(결함 검출 능력) 측정 측정 시간이 오래 걸리고 복잡함, 도구 지원 필요
코드 커버리지 맹신을 피하는 테스트 커버리지 측정 및 개선 실전 체크리스트: 브랜치, 조건, 돌연변이 커버리지 활용 - test, virus, coronavirus, self-test, covid-19, infection, lock down, hygiene, transmission, shutdown, pandemic, test, test, test, test, test, virus, virus, virus, virus, coronavirus, coronavirus, coronavirus, coronavirus, covid-19, covid-19, covid-19, covid-19, pandemic, pandemic

Image by Alexandra_Koch on Pixabay

실전 해결 과정: 견고한 테스트를 위한 커버리지 확장 체크리스트

저희 팀은 문제의 원인을 파악한 후, 테스트 커버리지 측정 및 개선 방식을 전면적으로 재검토했습니다. 단순히 수치를 높이는 것이 아니라, 실제 버그를 잡는 테스트의 품질을 높이는 데 집중하기로 했죠. 다음은 그 과정에서 우리가 활용했던 실전 체크리스트입니다.

1단계: 목표 설정 및 도구 도입

  • 라인 커버리지 맹목적 목표 지양: 더 이상 "라인 커버리지 몇 % 달성"을 유일한 목표로 삼지 않았습니다.
  • 브랜치/조건 커버리지 최소 목표 설정: 핵심 비즈니스 로직, 특히 결제, 보안, 데이터 처리와 같은 고위험 모듈에 대해서는 브랜치 커버리지 80% 이상, 조건 커버리지 70% 이상을 달성하는 것을 목표로 삼았습니다. 모든 코드에 100%를 적용하는 것은 비현실적이거든요.
  • 도구 도입 및 통합:
    • 기본 커버리지 측정 도구: Java 환경에서는 JaCoCo, Python에서는 Coverage.py, JavaScript에서는 Istanbul/Jest를 활용했습니다.
    • 뮤테이션 테스트 도구: Java를 주로 사용하는 저희 팀은 PIT(Program In Transformation)를 도입했습니다. JavaScript 프로젝트에서는 Stryker Mutator가 좋은 대안이 될 수 있습니다. 이러한 도구들을 CI/CD 파이프라인에 통합하여 주기적으로 측정하고 보고서를 생성하도록 자동화했죠.

2단계: 코드베이스 진단 및 개선 전략 수립

  • 핵심 모듈 집중 분석: 전체 코드베이스를 한 번에 개선하기는 어렵습니다. 앞서 언급한 결제 모듈처럼 버그 발생 확률이 높거나 비즈니스 임팩트가 큰 핵심 모듈부터 브랜치/조건 커버리지 보고서를 면밀히 검토했습니다.
  • 테스트 코드 리팩토링: 기존 테스트 코드가 라인 커버리지 위주로 작성되어 있다면, 모든 분기(Branch)와 조건(Condition)을 만족시키도록 테스트 케이스를 확장했습니다.

예를 들어, 다음과 같은 할인율 계산 함수가 있다고 해봅시다.

public double calculateDiscount(int age, boolean isVIP, double purchaseAmount) {
    double discountRate = 0.0;
    if (age > 60 && isVIP) { // 조건 1: age > 60, 조건 2: isVIP
        discountRate = 0.15; // 15% 할인
    } else if (age > 60 || purchaseAmount > 100000) { // 조건 3: age > 60, 조건 4: purchaseAmount > 100000
        discountRate = 0.10; // 10% 할인
    } else {
        discountRate = 0.05; // 5% 할인
    }
    return discountRate;
}

이 함수에 대해 라인 커버리지 100%를 달성하더라도, 조건 커버리지를 위해서는 단순히 age > 60 && isVIP가 true인 경우와 false인 경우만 테스트하는 것으로는 부족합니다. 각 조건(age > 60, isVIP, purchaseAmount > 100000)이 개별적으로 true/false를 갖는 모든 조합을 테스트해야 하죠. 예를 들어:

  • age = 65, isVIP = true, purchaseAmount = 50000 (15% 할인)
  • age = 65, isVIP = false, purchaseAmount = 50000 (10% 할인 - age > 60만 true)
  • age = 50, isVIP = true, purchaseAmount = 150000 (10% 할인 - purchaseAmount > 100000만 true)
  • age = 50, isVIP = false, purchaseAmount = 50000 (5% 할인)

이런 식으로 복합 조건문을 꼼꼼하게 파고드는 테스트 케이스를 작성하는 겁니다.

3단계: 지속적인 측정 및 팀 문화 정착

  • CI/CD 파이프라인 통합: Git 푸시 시, PR 생성 시 자동으로 브랜치/조건 커버리지가 측정되고, 설정된 임계값 미만일 경우 머지가 불가능하도록 게이트(Gate)를 설정했습니다. 이는 테스트 품질을 강제하는 효과적인 방법이었습니다.
  • 뮤테이션 테스트 전략적 활용: 뮤테이션 테스트는 계산 비용이 높아 전체 코드베이스에 상시 적용하기는 부담이 컸습니다. 그래서 우리는 다음과 같은 전략을 세웠어요.
    • 새로운 핵심 기능 개발 시: 해당 기능의 핵심 로직에 대해 뮤테이션 테스트를 수행하여 작성된 단위 테스트의 결함 검출 능력을 검증.
    • 리팩토링 시: 기존 레거시 코드의 리팩토링 시에도 뮤테이션 테스트를 통해 기존 테스트 스위트가 변경 사항을 잘 방어하는지 확인.
  • 코드 리뷰 시 커버리지 보고서 함께 검토: 코드 리뷰 단계에서 단순히 로직의 정확성뿐만 아니라, 함께 생성된 커버리지 보고서를 확인하며 놓친 분기나 조건은 없는지 동료들과 함께 점검하는 문화를 만들었습니다.

교훈 및 심화 논의: 품질과 비용 사이의 현명한 선택

완벽한 커버리지는 환상, 최적의 균형점 찾기

이 과정을 거치면서 우리는 중요한 교훈을 얻었습니다. 모든 코드를 100% 브랜치, 조건, 돌연변이 커버리지로 만드는 것은 현실적으로 어렵고, 막대한 시간과 비용을 소모한다는 점이죠. 중요한 것은 완벽함이 아니라 최적의 균형점을 찾는 것입니다.

  • 핵심 비즈니스 로직에 집중: 시스템의 핵심이 되는 부분, 고위험 모듈, 자주 변경되는 부분에 고급 커버리지 측정을 집중하고, 나머지 부분은 효율적인 수준에서 관리하는 것이 현명합니다.
  • 테스트 피라미드 활용: 단위 테스트에서 브랜치/조건/돌연변이 커버리지를 높여 코드의 세부 로직을 견고하게 만듭니다. 반면, 통합 테스트나 E2E 테스트는 핵심 사용자 시나리오와 시스템 간의 연동에 집중하여 비용 효율적인 테스트 전략을 구사하는 거죠.

커버리지 지표를 넘어선 "좋은 테스트"의 의미

결국 코드 커버리지는 수단이지 목적이 아닙니다. 이 지표들은 우리가 놓치고 있는 테스트의 사각지대를 알려주는 "도구"일 뿐이죠. 진정으로 중요한 것은 작성된 테스트의 품질, 즉 실제 결함을 얼마나 잘 검출해낼 수 있는가입니다. 돌연변이 커버리지가 이를 가장 직접적으로 측정해주는 지표라고 할 수 있겠네요.

더 나아가, 테스트 커버리지 외에도 요구사항 커버리지 (고객 요구사항이 테스트로 잘 커버되었는지), 결함 커버리지 (발견된 결함이 테스트로 재현되고 수정 후 통과하는지) 등 다양한 관점의 지표들을 함께 고려하여 전반적인 소프트웨어 품질을 향상시키는 노력이 필요합니다.

코드 커버리지 맹신은 위험하지만, 그 너머의 지표들을 이해하고 활용한다면 훨씬 더 견고하고 신뢰할 수 있는 소프트웨어를 만들 수 있습니다. 높은 라인 커버리지에 안주하지 않고, 브랜치, 조건, 돌연변이 커버리지를 통해 테스트의 깊이를 더해보는 건 어떨까요? 분명 팀의 생산성과 소프트웨어의 품질 모두 한 단계 끌어올릴 수 있을 겁니다.

여러분은 어떤 테스트 커버리지 전략을 사용하시나요? 혹은 뮤테이션 테스트를 도입하며 겪었던 특별한 경험이 있으신가요? 댓글로 자유롭게 여러분의 인사이트를 공유해주세요!

📌 함께 읽으면 좋은 글

  • [모바일 앱 개발] AR 산업 현장 솔루션 도입, 테크리드가 알아야 할 6가지 오해
  • [AI 머신러닝] 음성 인식 시스템을 오디오-의미론적 이해 모델로 업그레이드하며 얻은 교훈
  • [테스트 QA] E2E 테스트 불안정성, 드디어 잡았다: 재시도와 동기화 대기 전략 실전 비교 분석

이 글이 도움이 되셨다면 공감(♥)댓글로 응원해 주세요!
궁금한 점이나 다루었으면 하는 주제가 있다면 댓글로 남겨주세요.

반응형