본문으로 건너뛰기
중급24

구조 기반 기법 — 구문·분기·조건·MC-DC 커버리지

명세가 아니라 실제 코드를 근거로 테스트한다. 구문·분기·조건 커버리지의 차이를 코드로 직접 계산해본다.

  • #구조기반
  • #화이트박스
  • #커버리지
  • #MC-DC

개념

지금까지 배운 기법(동등분할, 경계값, 상태전이, 결정테이블, 유즈케이스, 페어와이즈, 분류 트리)은 전부 명세 기반이다 — 코드를 한 줄도 안 보고 만들 수 있다. 구조 기반 기법은 반대다. 실제 코드의 구조를 보고, 그 구조가 얼마나 실행됐는지를 기준으로 테스트를 설계하거나 평가한다.

커버리지 네 단계

같은 코드로 단계별 차이를 본다.

function getShippingFee(amount, isMember) {
  if (amount >= 30000 && isMember) {
    return 0;           // 무료배송
  }
  return 3000;           // 배송비 3000원
}

구문 커버리지(Statement Coverage): 모든 줄이 최소 한 번 실행됐는가. getShippingFee(50000, true) 하나만 호출해도 두 줄(if 안, return 3000은 실행 안 됨)... 실제로는 if문 안 코드만 실행되고 return 3000;은 실행이 안 되므로 구문 커버리지는 100%가 아니다. getShippingFee(50000, false)를 추가로 호출해야 return 3000; 줄도 실행되어 구문 커버리지 100%가 된다.

분기 커버리지(Branch/Decision Coverage): if문의 참(True)과 거짓(False) 결과가 각각 한 번씩 나왔는가. 위 두 호출로 이미 참·거짓이 한 번씩 나왔으므로 분기 커버리지도 100%다.

조건 커버리지(Condition Coverage): amount >= 30000isMember 각각의 개별 조건이 참·거짓을 모두 거쳤는가. 앞의 두 호출(50000, true50000, false)만으로는 amount >= 30000이 거짓인 경우가 한 번도 없었다. getShippingFee(10000, true)를 추가해야 조건 커버리지도 채워진다.

MC/DC (Modified Condition/Decision Coverage): 여기서 한 단계 더 나아가, 각 개별 조건이 전체 결과에 독립적으로 영향을 미치는지까지 확인한다. 즉 "isMember를 참에서 거짓으로 바꿨을 때, 다른 조건은 그대로인 채로 전체 결과가 바뀌는 케이스"가 있어야 한다. 조건이 2개일 때도 이걸 만족하는 케이스 조합을 고르는 게 생각보다 까다롭다 — 항공기 소프트웨어(DO-178C), 자동차(ISO 26262) 같은 안전이 중요한 산업의 표준이 이 수준을 요구한다. 부록에서 관련 표준을 다시 다룬다.

커버리지의 함정

100% 구문 커버리지가 "버그가 없다"는 뜻은 절대 아니다. 위 함수에서 할인 로직이 >= 대신 >로 잘못 짜여 있어도(경계값 결함), 그 코드 줄 자체는 실행됐으니 구문 커버리지는 여전히 100%가 나올 수 있다. 커버리지는 "어떤 코드가 실행됐는가"만 말해줄 뿐, "그 결과가 맞는지 제대로 검증했는가"는 말해주지 않는다. 이 함정은 모듈 10(단위·통합 테스트)에서 더 깊이 다룬다.

도구

SonarQube는 일반적인 구문·분기 커버리지 리포트를 보여주는 대중적인 도구다. VectorCAST, Test RealTime, LDRA 같은 도구는 MC/DC까지 측정·인증할 수 있어 안전이 중요한 임베디드·항공·자동차 산업에서 주로 쓰인다.

실무에서 왜 필요한가

명세 기반 기법만으로는 "코드에 있지만 요구사항 문서엔 언급 안 된 예외 처리 코드"를 놓칠 수 있다 — 예를 들어 개발자가 방어적으로 짜둔 에러 핸들링 분기는 명세서 어디에도 안 적혀 있을 수 있다. 구조 기반 기법은 이런 "코드에는 있지만 명세엔 없는" 경로를 찾아준다. 반대로 구조 기반만 쓰면 "코드가 원래 요구사항을 제대로 구현했는가"는 못 본다 — 그래서 이 모듈 초반(레슨 2)에서 강조했듯이 두 갈래는 항상 함께 쓰는 게 이상적이다.

실습 과제

과제 1 — 커버리지 직접 계산하기 (15분)

다음 함수에 대해 isEligible(20, true) 딱 한 번만 테스트했다고 하자.

def is_eligible(age, has_id):
    if age >= 18 and has_id:
        return True
    return False

이 한 번의 호출로 구문 커버리지는 몇 %인지, 분기 커버리지는 몇 %인지 계산한다. 100% 분기 커버리지를 채우려면 어떤 추가 호출이 필요한지 적는다.

과제 2 — 커버리지 함정 설명하기 (8분)

"이 코드는 구문 커버리지 100%이니 안심하고 배포해도 된다"는 동료의 말에 어떻게 반박할지, 위 개념에서 배운 내용을 근거로 2~3문장으로 적는다.

자가 체크리스트

  • 구문·분기·조건 커버리지의 차이를 코드 예시로 계산할 수 있다
  • MC/DC가 조건 커버리지와 어떻게 다른지, 왜 더 엄격한지 설명할 수 있다
  • "100% 커버리지 = 버그 없음"이 아닌 이유를 설명할 수 있다
  • 구조 기반 기법이 명세 기반 기법으로 못 찾는 무엇을 찾아주는지 설명할 수 있다

흔한 실수

  • 커버리지 %를 품질의 척도로 오해한다. 커버리지는 "실행 여부"만 잰다. "검증이 제대로 됐는지"는 어서션(assert)의 품질에 달려 있다 — 이건 모듈 10에서 자세히 다룬다.
  • 분기 커버리지와 조건 커버리지를 같은 것으로 착각한다. A && B 형태의 복합 조건에서는 분기 커버리지가 100%여도 A, B 개별 조건은 둘 다 참/거짓을 거치지 않았을 수 있다.
  • 모든 프로젝트에 MC/DC 수준을 요구한다. MC/DC는 비용이 크다 — 안전이 중요한 임베디드·항공·의료기기 소프트웨어가 아니라면 보통 분기 커버리지 수준으로 충분하다.

참고 자료