코드 검사 다음은 실행이다
16장에서 다룬 코드 검사는 프로그램을 실행하지 않고 사람의 눈으로 오류를 찾는 정적 점검이었다. 이번 장에서 다루는 소프트웨어 테스트는 반대로 프로그램을 실제로 돌려보며 결함을 찾는 동적 점검이다. 1장에서 소프트웨어 개발의 여섯 단계 중 다섯 번째로 "시험"을 소개하며 단위-통합-시스템-인수라는 네 층위가 있다고 예고했는데, 이번 장에서 그 네 층위를 실제로 어떻게 설계하고 수행하는지 깊이 들어간다.
책마루 온라인 주문 시스템에서 가장 골치 아픈 버그 후보를 하나 미리 소개하면 이렇다. 손님 두 명이 거의 동시에 마지막 남은 재고 1개짜리 상품을 주문하면 어떻게 될까? 재고 차감 로직이 "재고를 확인하고, 확인한 값을 기준으로 차감한다"는 두 단계로 짜여 있다면, 두 주문이 동시에 재고를 "1개 있음"으로 확인한 뒤 각자 차감을 시도해 재고가 -1이 되는 상황이 벌어질 수 있다. 이런 동시성 버그는 코드를 눈으로 읽는 검사만으로는 거의 발견되지 않는다 — 실제로 여러 요청이 겹치는 상황을 만들어 실행해봐야 드러난다. 이 장 전체에서 이 버그를 어떤 테스트 층위와 기법으로 잡아낼 수 있는지 계속 따라가 본다.
테스트가 증명할 수 있는 것과 증명할 수 없는 것
소프트웨어 테스트의 표면적인 목적은 명확하다 — 결함을 찾아내고, 소프트웨어가 요구사항과 명세를 충족하는지 확인하고, 출시 이후 발생할 결함을 미리 예방하는 것이다. 여기에 조금 더 실무적인 목적을 더하면, 테스트는 "이 소프트웨어를 지금 배포해도 되는가"에 대한 근거 있는 확신을 관계자들에게 제공하는 역할도 한다.
다만 테스트를 대하는 태도에서 한 가지 반드시 짚어야 할 지점이 있다. 테스트를 아무리 많이 통과해도 그 프로그램에 결함이 전혀 없다는 것을 증명할 수는 없다는 것이다. 테스트가 보여줄 수 있는 것은 오직 "이 테스트로는 결함이 발견되었다" 또는 "이 테스트로는 결함이 발견되지 않았다"뿐이다. 책마루 재고 차감 함수를 100개의 테스트 케이스로 돌려 전부 통과했다 해도, 101번째로 시도할 특정 조합(가령 동시 주문)에서 버그가 없다고 보장할 수는 없다. 이 한계 때문에 테스트 설계의 실질적인 목표는 "모든 경우를 다 확인하는 것"이 아니라 "가장 적은 수의 테스트 케이스로 가장 많은 결함을 찾아내는 것"으로 좁혀진다. 좋은 테스트 케이스란 결국 아직 발견되지 않은 결함을 드러낼 확률이 높은 케이스를 뜻한다.
개발 프로세스와 테스트를 잇는 V모델
테스트를 "코딩이 다 끝난 뒤 마지막에 한 번에 몰아서 하는 작업"으로 여기기 쉽지만, 실제로는 테스트 계획과 설계가 개발 단계마다 함께 진행된다. 이 관계를 시각화한 것이 V모델이다.
개발 프로세스를 요구사항 분석 → 기본설계(아키텍처) → 상세설계(모듈설계) → 코딩 순서로 내려가는 왼쪽 축이라 하면, 오른쪽 축은 코딩에서 다시 올라오며 단위 시험 → 통합 시험 → 시스템 시험 → 인수 시험으로 이어진다. 이 둘을 이으면 알파벳 V자 모양이 되는데, 중요한 건 각 왼쪽 단계가 같은 높이의 오른쪽 테스트 단계와 직접 대응한다는 점이다.
| 개발 단계 | 대응하는 테스트 단계 | 책마루 예시 |
|---|---|---|
| 요구사항 분석 | 인수 시험 | "우편번호로 배달 가능 여부를 확인한다"는 요구사항 → 사장님이 실제로 우편번호를 입력해 확인 |
| 기본설계(아키텍처) | 시스템 시험 | 주문 서버·재고 DB·PG 연동을 아키텍처로 나눔 → 전체가 조합되어 돌아가는지 확인 |
| 상세설계(모듈설계) | 통합 시험 | 재고 모듈과 주문 모듈의 인터페이스 정의 → 두 모듈을 합쳐 인터페이스가 맞는지 확인 |
| 코딩 | 단위 시험 | 재고 차감 함수 코드 작성 → 그 함수 하나만 떼어 확인 |
이 대응 관계가 의미하는 바는, 예컨대 시스템 시험에 쓸 테스트 계획을 코딩이 끝난 뒤가 아니라 기본설계 단계에서 미리 준비해둔다는 것이다. 설계할 때 "이 아키텍처가 제대로 됐는지 어떻게 확인할 것인가"를 함께 고민하면, 설계 자체도 검증 가능한 형태로 다듬어지는 효과가 있다. 책마루 프로젝트라면 기본설계 단계에서 이미 "재고 동기화가 매장 POS와 5초 이내 지연으로 이루어져야 한다"는 시스템 시험 계획을 세워두는 식이다.
네 층위의 테스트
단위 테스트 — 가장 작은 조각부터
단위 테스트는 프로그램의 가장 작은 단위인 모듈(보통 함수 하나) 하나만 떼어내 검증한다. 책마루의 deductStock(productId, quantity) 함수라면, 재고가 충분할 때 정상적으로 줄어드는지, 재고보다 많은 수량을 요청했을 때 거부되는지, 수량이 0이나 음수로 들어왔을 때 어떻게 처리하는지를 이 함수 하나만 실행해 확인한다. V모델에서 단위 테스트는 테스트 프로세스의 첫 단계이며, 동시에 코드가 코딩 표준을 지켰는지도 함께 짚는 자리다.
통합 테스트 — 모듈을 이어 붙였을 때
모듈 하나하나가 잘 작동한다고 해서 그것들을 이어 붙인 결과까지 잘 작동하는 것은 아니다. 통합 테스트는 재고 모듈과 주문 모듈처럼 개별 컴포넌트를 결합했을 때 그 사이의 인터페이스와 상호작용에서 발생하는 오류를 찾는다. 예를 들어 주문 모듈이 재고 모듈에 상품 ID를 문자열로 넘기는데 재고 모듈은 숫자를 기대하고 있다면, 각 모듈은 단위 테스트를 각각 통과했더라도 통합하는 순간 오류가 드러난다.
모듈을 한 번에 전부 이어 붙여 테스트하면 오류가 발생했을 때 그 원인이 어느 모듈에 있는지 찾기가 매우 어려워진다. 그래서 실무에서는 점진적(incremental) 통합을 쓴다 — 한 번에 하나씩 모듈을 추가하며 테스트하는 방식이다. 이 점진적 통합에는 두 가지 방향이 있다.
- 하향식 통합: 최상위 모듈부터 시작해 하위 모듈을 하나씩 붙여나간다. 책마루라면 "주문 처리" 모듈을 먼저 두고, 아직 만들어지지 않은 하위 모듈(재고 확인, 결제 처리)은 임시로 흉내만 내는 스텁(stub)으로 대체한 뒤, 실제 모듈을 하나씩 교체해가며 테스트한다.
- 상향식 통합: 반대로 가장 아래의 모듈부터 결합해 위로 올라간다. 재고 차감, 결제 처리 같은 하위 모듈을 먼저 통합하고, 이들을 호출하는 상위의 "주문 처리" 모듈을 나중에 결합한다. 이때는 아직 만들어지지 않은 상위 모듈 대신 하위 모듈을 호출해주는 드라이버(driver)가 필요하다.
또한 통합 시험 계획은 상세설계 단계에서 미리 준비되고, 통합 시험 자체는 대개 블랙박스 방식(뒤에서 다룬다)으로 진행되며 보통 개발자가 직접 수행한다.
회귀 테스트를 함께 챙겨야 하는 이유: 새 모듈을 통합할 때마다 기존에 이미 통과했던 기능이 새 코드 때문에 망가지지 않았는지도 확인해야 한다. 이를 회귀 테스트(regression test)라 하며, 전체 기능을 대표하는 테스트 케이스를 재실행하고 특히 새 모듈의 영향을 받을 만한 기능에 집중해서 다시 확인한다. 책마루에서 "정기구독 배송" 기능을 새로 통합했다면, 이미 잘 작동하던 "동네 배달" 기능이 여전히 정상인지도 함께 재확인해야 한다.
시스템 테스트 — 소프트웨어 혼자가 아니다
시스템 테스트는 소프트웨어를 하드웨어, 외부 서비스, 다른 소프트웨어까지 포함한 전체 시스템 안에서 검증한다. 책마루라면 결제대행사(PG) API, 택배사 연동, 매장 알림 태블릿까지 모두 연결한 상태에서 "손님이 주문하면 실제로 결제가 되고 태블릿에 알림이 뜨는가"를 확인하는 단계다. 시스템 테스트는 실제로 구현된 결과와 계획된 사양을 비교하는 작업이며, 모듈 통합이 끝난 뒤에 시작한다. 이 단계에서는 기능 확인뿐 아니라 시스템 차원의 비기능 요구도 함께 점검한다.
- 복구 테스트: PG사 서버가 갑자기 응답하지 않을 때 시스템이 정상적으로 복구되는가
- 보안 테스트: 결제 정보나 손님 개인정보가 외부에 노출되지 않는가
- 스트레스 테스트: 특정 시간(예: 점심시간 배달 주문 폭주)에 부하가 몰려도 시스템이 버티는가
- 성능 테스트: 주문 목록 로딩이 1장에서 정한 "3초 이내" 요구사항을 실제로 만족하는가
인수 테스트 — 판단은 결국 고객의 몫이다
개발된 소프트웨어가 사용자의 요구를 만족하는지 확인하는 테스트를 확인 테스트(validation testing)라 하는데, 이 확인 테스트를 개발자나 독립적인 테스트 그룹이 아니라 실제 사용자가 수행할 때 이를 인수 테스트라 부른다. 인수 테스트는 시스템 테스트가 끝난 뒤 시작되며, 요구사항 분석 단계에서 정의된 사양이나 계약을 실제로 만족하는지를 최종 확인한다.
인수 테스트는 보통 두 단계로 나뉜다. 사장님(또는 개발팀 내부 관계자)이 개발 환경에서 직접 사용해보는 알파 테스트와, 실제로 몇몇 단골 손님에게 배포해 그들의 실제 환경에서 사용해보게 하는 베타 테스트다. 책마루라면 알파 테스트 단계에서 사장님이 매장 태블릿으로 직접 주문을 넣어보며 "포장 목록이 프린터로 잘 나오는가"를 확인하고, 베타 테스트 단계에서는 실제 단골 손님 몇 명에게 미리 시스템을 열어주고 "우편번호를 입력했을 때 배달 가능 여부가 정확히 뜨는가"를 실사용 환경에서 확인받는 식이다.
무엇을 기준으로 테스트 케이스를 만드는가: 블랙박스와 화이트박스
네 층위 중 어느 단계든 결국 "어떤 입력값으로 테스트할 것인가"를 정해야 한다. 이 테스트 케이스를 설계하는 접근법은 크게 두 가지로 나뉜다.
블랙박스 테스트 — 내부를 몰라도 된다
블랙박스 테스트는 프로그램의 내부 구조를 전혀 참조하지 않고, 정해진 입력을 넣었을 때 요구된 출력이 나오는지만 확인한다. 코드를 몰라도 요구사항 명세서만 있으면 테스트 케이스를 만들 수 있다는 뜻이다. 이 접근에서 자주 쓰이는 기법 몇 가지를 책마루 예제로 살펴보면 이렇다.
| 기법 | 설명 | 책마루 적용 예 |
|---|---|---|
| 동등 분할 | 입력을 같은 종류의 오류를 낼 만한 그룹으로 나눠 각 그룹에서 대표값만 테스트 | 재고 수량 입력을 "정상 범위", "음수", "0"으로 나눠 각각 대표값 테스트 |
| 경계값 분석 | 동등 분할로 나눈 그룹의 경계 근처 값을 집중 테스트 | 재고가 정확히 1개 남았을 때 주문 수량 1, 그리고 재고 0일 때 주문 시도 |
| 결정 테이블 | 여러 조건의 조합에 따른 기대 결과를 표로 정리해 테스트 케이스 도출 | "배달 가능 지역 여부 × 결제 성공 여부"의 네 조합별 기대 동작 정리 |
| 상태 전이 | 상태 변화 흐름(전이도)을 기반으로 각 전이 경로를 테스트 | 주문 상태가 "접수→포장중→배달중→완료"로 바뀌는 각 전이 확인 |
이 중 경계값 분석이 특히 유용한 이유는, 실무에서 오류가 몰리는 지점이 대부분 "딱 그 경계"이기 때문이다. 재고가 넉넉히 10개 있을 때는 테스트해도 버그가 잘 안 보이지만, 재고가 정확히 1개 남은 순간 동시에 여러 주문이 들어오는 경계 상황이야말로 앞서 소개한 재고 동시성 버그가 실제로 드러나는 지점이다.
화이트박스 테스트 — 코드 내부를 들여다본다
화이트박스 테스트는 반대로 프로그램의 내부 구조를 조사해 그 구조를 기준으로 테스트 케이스를 설계한다. 그래서 구조적(structural) 테스트 또는 코드 기반(code-based) 테스트라고도 불린다. 대표적인 검증 기준은 다음과 같다.
| 검증 기준 | 설명 |
|---|---|
| 문장 커버리지 | 코드의 모든 실행문이 적어도 한 번은 실행되도록 테스트 케이스 설계 |
| 분기 커버리지 | 모든 결정문(if 등)의 참/거짓 분기가 각각 한 번은 실행되도록 설계 |
| 조건 커버리지 | 결정문을 구성하는 개별 조건식의 참/거짓까지 각각 실행되도록 설계 |
| 다중 조건 커버리지 | 조건들의 모든 조합이 실행되도록 설계 |
여기서 중요한 함정 하나를 책마루 코드로 직접 확인해보자. 재고 확인 로직이 다음과 같이 짜여 있다고 하자.
if (stock > 0 and reserved == false) then
confirmOrder()
endif
이 코드를 문장 커버리지 기준으로만 테스트하면, "stock=5, reserved=false"라는 케이스 하나만으로도 confirmOrder()가 실행되는 문장을 한 번 지나가므로 기준을 만족해버린다. 그런데 만약 원래 의도와 다르게 코드가 stock > 0 or reserved == false로 잘못 작성되어 있었다면 어떨까? 이 잘못된 조건에서도 앞의 같은 입력값은 여전히 참이 되어 confirmOrder()가 실행되므로, 문장 커버리지를 만족하는 테스트로는 이 논리 오류를 전혀 잡아내지 못한다. and와 or가 뒤바뀐 이 흔한 실수는 개별 조건의 참/거짓 조합을 따로 확인하는 조건 커버리지나 다중 조건 커버리지 수준까지 가야 비로소 드러난다. 결국 커버리지 기준이 높을수록 더 많은 오류를 잡아내지만, 모든 가능한 테스트 케이스를 다 만드는 것은 애초에 불가능하다는 점은 화이트박스 테스트에도 똑같이 적용된다.
책마루의 재고 동시성 버그로 돌아가서
이 장 서두에서 던진 재고 동시성 버그를 지금까지 배운 개념으로 다시 짚어보자. 이 버그는 단위 테스트만으로는 잡히지 않는다 — deductStock() 함수를 혼자 호출해서는 "동시에 두 요청이 들어온다"는 상황 자체가 재현되지 않기 때문이다. 이 버그는 통합 테스트 또는 시스템 테스트 단계에서, 그것도 일반적인 순차 실행 테스트가 아니라 "동시에 같은 재고에 접근하는 두 요청"이라는 경계 상황을 의도적으로 만든 테스트 케이스로만 드러난다.
즉 좋은 테스트 케이스란 코드가 정상적으로 짜여 있을 때 통과하는 케이스가 아니라, 코드가 잘못 짜여 있을 가능성이 높은 지점 — 경계값, 동시 접근, 예외 조건 — 을 겨냥한 케이스다. 책마루 프로젝트라면 "동일 상품에 대해 재고 1개 상태에서 두 개의 주문 요청을 거의 동시에 보낸다"는 테스트 케이스를 시스템 테스트 단계에 명시적으로 포함시켜야, 오픈 이후 실제 손님들이 몰리는 상황에서 이 버그를 마주치기 전에 미리 잡아낼 수 있다.
핵심 정리
- 테스트는 결함이 없음을 증명하는 활동이 아니라 결함을 찾아내는 활동이다. 목표는 최소한의 테스트 케이스로 최대한 많은 결함을 발견하는 것이다.
- V모델은 요구사항 분석-인수 시험, 기본설계-시스템 시험, 상세설계-통합 시험, 코딩-단위 시험이 서로 대응하며, 테스트 계획은 코딩이 끝난 뒤가 아니라 각 설계 단계에서 함께 준비된다.
- 단위 테스트는 모듈 하나, 통합 테스트는 모듈 간 인터페이스, 시스템 테스트는 하드웨어·외부 연동을 포함한 전체, 인수 테스트는 실제 사용자의 눈으로 검증한다.
- 블랙박스 테스트는 요구사항을 기준으로, 화이트박스 테스트는 코드 구조를 기준으로 테스트 케이스를 설계하며, 두 접근은 서로 다른 종류의 결함을 잡아내므로 병행해야 한다.
- 동시성 버그처럼 특정 경계 상황에서만 드러나는 결함은 그 상황을 의도적으로 겨냥한 테스트 케이스 없이는 발견되지 않는다.
책마루 재고 동시성 버그를 테스트로 발견했다고 해서 끝이 아니다. 오픈 이후 이 버그를 실제로 수정하고, 그 수정이 다른 기능에 영향을 주지 않는지 확인하고, 이후에도 계속 시스템을 고쳐나가는 활동이 이어진다. 다음 장(18장)에서는 이 오픈 이후의 활동, 즉 유지보수를 네 가지 유형으로 나누어 다룬다.