개념
먼저 결론부터
이 레슨의 결론은 한 문장이다. 테스트는 전수 검사가 아니라 표본 추출이다.
QA를 처음 시작할 때 가장 흔한 오해가 "테스트란 모든 경우를 다 확인해서 버그가 없음을 확인하는 일"이라는 것이다. 이건 불가능하다 — 시간이 부족해서가 아니라 수학적으로 불가능하다. 그러면 QA는 무엇을 하는 사람인가? 다 볼 수 없는 공간에서 어디를 볼지 근거를 갖고 고르고, 무엇을 안 봤는지까지 말할 수 있는 사람이다.
이 레슨은 그 "불가능"을 숫자로 확인하고, 거기서 자연스럽게 나오는 세 가지 실무 능력으로 이어진다.
- 조합이 왜 폭발하는지 계산으로 설명하기
- 무엇을 기준으로 표본을 고를지 축을 갖기
- "버그 없이 나가죠?"에 근거 있게 답하기
조합은 덧셈이 아니라 곱셈으로 늘어난다
간단해 보이는 폼도 모든 입력 조합을 따지면 숫자가 순식간에 감당 못 할 크기가 된다. 핵심은 조합이 더해지는 게 아니라 곱해진다는 점이다. 필드를 하나 추가하면 케이스가 몇 개 늘어나는 게 아니라 몇 배가 된다.
회원가입 폼에 필드 5개(이메일, 비밀번호, 닉네임, 생년월일, 약관 동의)가 있고, 각 필드에 "유효값 / 경계값 / 비유효값" 정도로만 값을 잡아도 필드당 최소 3가지 경우가 나온다.
5개 필드 × 3가지 경우 = 3⁵ = 243가지 조합. 아직은 해볼 만해 보인다. 하지만 여기서 멈추면 실무 감각이 생기지 않는다. 진짜 문제는 이 숫자가 현실에서 얼마나 더 커지느냐다.
실무 규모로 다시 계산해보자
같은 회원가입 화면을 실무에서 보는 방식으로 다시 세어보자. 한 줄씩 현실적인 조건을 더할 때마다 숫자가 어떻게 되는지가 이 표의 전부다.
| 단계 | 무엇을 더했나 | 배수 | 누적 조합 수 |
|---|---|---|---|
| 1 | 필드 5개 × 값 3가지 | — | 243 |
| 2 | 값을 실무 수준으로(필드당 6가지) | ×32 | 7,776 |
| 3 | 회원 상태 3가지(비로그인 / 초대링크 / 휴면복구) | ×3 | 23,328 |
| 4 | 디바이스·브라우저 조합 8가지 | ×8 | 186,624 |
| 5 | 언어·통화 4가지 | ×4 | 746,496 |
| 6 | 앞뒤로 이어지는 화면 3개(각 20가지 경로) | ×8,000 | 약 60억 |
2단계의 "필드당 6가지"는 부풀린 숫자가 아니다. 이메일 하나만 봐도
정상값, 빈값, @ 없음, 도메인 없음, 최대 길이 초과, 앞뒤 공백 포함, 대문자
혼용, 이미 가입된 주소… 6가지는 오히려 후한 편이다.
이제 시간으로 바꿔보자. 이 60억 가지를 완전히 자동화해서 한 건당 10초에 돌린다고 하자.
- 60억 × 10초 = 600억 초 ≈ 약 1,900년
- 장비 100대를 병렬로 돌려도 약 19년
기능 하나짜리 화면 묶음에 19년이다. 그사이 제품은 수백 번 바뀐다. 그래서 소진적 테스트(exhaustive testing, 모든 입력과 조건의 조합을 다 실행하는 것)는 현실적으로 어렵다가 아니라 원리적으로 불가능하다.
필드 하나로도 이미 불가능하다
조합을 여러 개 곱하지 않아도 결론은 같다. 숫자를 입력받는 필드가 딱 하나만 있다고 하자. 32비트 정수라면 가능한 값은 2³² = 약 42억 9천만 가지다. 초당 1,000건을 검사해도 약 50일이 걸린다. 필드가 두 개면 1.8×10¹⁹ 가지 — 같은 속도로 5억 8천만 년이다.
여기에 아직 아무것도 안 더했다. 시간(자정 넘김, 윤년, 서머타임), 순서(뒤로가기, 새로고침, 두 번 클릭), 동시성(같은 계정으로 두 창에서 동시에), 네트워크 상태(끊김, 느림, 중복 응답)… QA가 실제로 마주치는 결함의 상당수는 이런 입력 값이 아닌 차원에서 나온다. 그 차원까지 곱하기 시작하면 숫자는 세는 의미가 없어진다.
데이크스트라의 말 — 테스트가 증명할 수 있는 것
그래서 에츠허르 데이크스트라(Edsger Dijkstra)의 유명한 말이 있다.
"테스트는 결함이 있다는 것을 보여줄 수 있을 뿐, 결함이 없다는 것을 보여주지는 못한다." (Testing shows the presence of defects, not their absence)
논리적으로 보면 이유가 분명하다. 테스트 한 건이 실패하면 "결함이 있다"는 결론이 곧바로 나온다. 반례 하나면 충분하기 때문이다. 반대로 테스트가 전부 통과했을 때 나오는 결론은 "결함이 없다"가 아니라 "내가 시도한 조건에서는 문제가 보이지 않았다" 뿐이다. 60억 중 500건을 통과시킨 것으로 나머지 59억 9999만 건을 보증할 수는 없다.
이 비대칭을 몸에 익히는 것이 이 레슨의 핵심이다. 그래서 결함 리포트에 "버그 있음"은 단정해서 쓰고, 통과 보고에 "버그 없음"은 절대 단정해서 쓰지 않는다.
그래서 테스트는 표본 추출이다
모든 조합을 볼 수 없으니, QA는 어디를 볼지 전략적으로 고른다. 여론조사를 떠올리면 감이 온다. 5천만 명에게 다 물어볼 수 없으니 1,000명을 뽑아 묻는다. 이때 결과의 품질을 결정하는 건 "몇 명에게 물었나"보다 "어떻게 뽑았나" 다. 편향된 1,000명은 무의미하고, 잘 뽑은 1,000명은 꽤 정확하다.
테스트도 똑같다. 케이스를 1,000건 실행했다는 사실 자체는 품질을 말해주지 않는다. 그 1,000건을 무슨 근거로 골랐는지가 품질을 말해준다. 여기서 QA의 일이 "많이 클릭하는 일"에서 "고르는 일"로 바뀐다.
무엇을 기준으로 표본을 고르는가 — 다섯 가지 축
표본을 고르는 기준은 하나가 아니다. 실무에서는 아래 축들을 겹쳐서 쓴다. 지금은 각 축의 상세 기법을 배우는 자리가 아니라 어떤 축들이 있는지 지도를 갖는 자리다. 각 축이 어디서 자세히 다뤄지는지는 오른쪽 열에 적었다.
| 축 | 던지는 질문 | 대표적인 접근 | 자세히는 |
|---|---|---|---|
| 입력 값의 구조 | 같은 취급을 받는 값 무리는 어디까지인가, 경계는 어디인가 | 동등분할, 경계값 분석, 결정 테이블, 페어와이즈 | 모듈 3 |
| 사용 빈도 | 사용자가 실제로 많이 지나는 길은 어디인가 | 사용 통계·운영 프로파일 기반 우선순위 | 모듈 4 |
| 리스크 | 망가졌을 때 손해가 큰 곳은 어디인가 | 발생 가능성 × 영향도 | 모듈 4 |
| 결함 이력 | 과거에 버그가 몰렸던 곳은 어디인가 | 결함 밀집(defect clustering) 분석 | 모듈 6 |
| 변경 이력 | 이번 릴리즈에서 실제로 바뀐 곳은 어디인가 | 변경 영향 분석, PR diff 읽기 | 모듈 9 |
이 중 결함 이력 축은 조금 반직관적이라 짚어둘 만하다. 결함은 코드 전체에 고르게 흩어지지 않고 특정 모듈에 몰리는 경향이 있다. 복잡하거나, 자주 바뀌거나, 여러 사람이 손댔거나, 급하게 만들어진 곳이다. 그래서 "지난 분기 결함이 가장 많았던 화면 3개"는 다음 분기에도 결함이 나올 확률이 높다. 이걸 결함 밀집(defect clustering) 이라고 부른다. 표본을 고를 때 과거 결함 이력만큼 값싸고 강한 근거가 드물다.
축을 여러 개 겹치면 우선순위가 자연스럽게 나온다. 예를 들어 "이번에 코드가 바뀌었고(변경) + 결제 흐름이며(리스크) + 작년에 결함이 3건 났던 화면(이력)"은 세 축에서 동시에 걸린다. 그런 곳에 시간을 몰아주는 것이 표본 추출 전략이다.
같은 테스트를 반복하면 효과가 줄어든다
표본을 잘 골랐다고 끝이 아니다. 같은 표본을 계속 쓰면 그 표본의 효과가 시간이 지날수록 떨어진다.
같은 테스트 스위트를 계속 똑같이 돌리면, 그 스위트가 잡을 수 있는 결함은 이미 다 잡혀서 더 이상 새로운 결함을 못 찾는다 — 이걸 살충제 패러독스(Pesticide Paradox) 라고 부른다. 같은 살충제를 계속 쓰면 벌레가 내성이 생기는 것에 빗댄 표현이다.
여기서 오해하기 쉬운 지점이 있다. 살충제 패러독스는 "회귀 테스트가 쓸모없다"는 뜻이 아니다. 기존 스위트는 이미 고친 것이 다시 깨지지 않았음을 확인하는 안전망으로서 여전히 유효하다(모듈 1 레슨 4의 회귀 테스트). 다만 그 스위트는 새로운 결함을 찾는 도구로는 점점 무력해진다. 두 가지 역할을 구분하지 않으면 "회귀 스위트가 다 통과했으니 품질이 좋다"는 잘못된 안심에 빠진다.
사례 연구 — 3년째 100% 통과하는 회귀 스위트
살충제 패러독스가 실무에서 어떤 모습으로 나타나는지, 하나의 흐름으로 따라가 보자.
상황. 이커머스 결제팀의 회귀 스위트는 420건이다. 매 릴리즈마다 자동으로 돌고, 최근 1년간 통과율은 거의 항상 100%였다. 팀은 이 숫자를 대시보드에 띄워두고 자랑스러워했다.
문제. 그런데 최근 6개월 동안 프로덕션(사용자가 실제로 쓰고 있는 환경) 장애가 5건 났다. 포인트 부분사용 시 잔액이 음수로 찍히는 문제, 간편결제 재시도에서 이중 결제가 되는 문제, 정기결제 갱신일이 월말에 밀리는 문제 등이다. 다섯 건 모두 회귀 스위트를 100% 통과한 릴리즈에서 발생했다.
1단계 — 케이스의 나이를 세어봤다. 420건이 각각 언제 만들어졌는지 확인했다.
| 작성 시기 | 케이스 수 | 비중 |
|---|---|---|
| 3년 전 최초 구축 | 312 | 74% |
| 2년 전 | 68 | 16% |
| 1년 전 | 33 | 8% |
| 최근 6개월 | 7 | 2% |
즉 이 스위트가 검사하고 있는 제품은 사실상 3년 전의 제품이었다. 그사이 추가된 간편결제, 포인트 부분사용, 정기결제 기능에 해당하는 케이스는 다 합쳐도 10건이 안 됐다.
2단계 — 최근 결함을 역추적했다. 지난 6개월에 발견된 결함 60건을 놓고 "이 결함을 기존 420건 중 어떤 케이스가 잡을 수 있었을까"를 하나씩 대조했다. 답은 7건이었다. 나머지 53건은 스위트의 사정권 밖에 있었다.
3단계 — 통과율 100%의 의미를 다시 읽었다. 통과율 100%가 오래 유지된다는 건 좋은 신호가 아니라 경고 신호였다. "이 스위트는 더 이상 새로운 정보를 만들어내지 못한다"는 뜻이기 때문이다. 매 릴리즈 40분씩 쓰면서 얻는 정보량이 0에 수렴하고 있었다.
4단계 — 처방. 팀이 실제로 한 조치는 네 가지다.
| 조치 | 내용 |
|---|---|
| 신규 기능 케이스 보강 | 최근 1년 내 추가된 기능에 우선 케이스를 채운다 |
| 값 로테이션 | 고정된 테스트 데이터(항상 같은 계정·금액·쿠폰)를 릴리즈마다 바꿔 돌린다 |
| 결함 피드백 루프 | 프로덕션 결함이 나오면 그 결함을 잡는 케이스를 회귀 스위트에 반드시 추가한다 |
| 탐색적 세션 배정 | 릴리즈마다 스크립트 없는 탐색적 테스팅 시간을 고정 배정한다(모듈 5) |
교훈. 스위트가 낡는 건 스위트를 잘못 만들어서가 아니라 제품이 움직이기 때문이다. 그래서 회귀 스위트는 만들어두고 방치하는 자산이 아니라 주기적으로 손봐야 하는 자산이다. 통과율만 보지 말고 "이 스위트가 최근에 잡아낸 결함이 몇 건인가"를 같이 봐야 한다.
커버리지 100%라는 말의 함정
"테스트 커버리지 100% 달성"이라는 말을 들으면 "이제 버그가 없겠구나" 싶다. 하지만 커버리지가 100%여도 결함은 남는다. 왜 그런지는 모듈 1 레슨 2에서 배운 도달·감염·전파로 설명하면 가장 깔끔하다.
레슨 2에서 결함이 실패가 되려면 세 가지가 다 필요하다고 배웠다. 커버리지는 그중 첫 번째 하나만 보증한다.
| 단계 | 의미 | 커버리지가 보증하는가 |
|---|---|---|
| 도달(Reachability) | 그 코드 줄이 실행됐다 | 예 — 구문 커버리지가 세는 게 정확히 이것이다 |
| 감염(Infection) | 실행 결과로 내부 상태가 잘못됐다 | 아니오 — 어떤 값을 넣었느냐에 달렸다 |
| 전파(Propagation) | 잘못된 상태가 밖으로 드러나고, 그걸 확인했다 | 아니오 — 단언(검증) 이 있어야 한다 |
구체적으로 보자. 아래 함수는 한 줄짜리라 아무 입력이나 한 번만 넣어도 구문 커버리지 100%가 된다.
function applyDiscount(price) {
return price - price * 0.1 // 기획서에는 15% 할인이라고 쓰여 있다
}applyDiscount(10000)을 실행하면 그 줄은 도달했다. 결과 9,000은 기대값
8,500과 달라 내부 상태도 감염됐다. 그런데 테스트가 결과를 확인하지
않거나, 기대값을 9,000으로 잘못 적어두었다면 전파가 일어나지 않아
테스트는 초록불로 통과한다. 커버리지 100%, 결함 그대로.
커버리지가 못 보는 구멍은 크게 세 가지다.
- 단언이 없거나 약한 테스트. 실행만 하고 결과를 확인하지 않으면 커버리지는 올라가지만 아무것도 검증하지 않는다. 커버리지 지표를 목표로 걸면 실제로 이런 테스트가 양산된다.
- 코드에 아예 없는 요구사항. 기획서에는 있는데 구현이 통째로 빠졌다면, 그 코드는 존재하지 않으므로 커버리지에도 잡히지 않는다. 누락 결함은 커버리지가 원리적으로 못 잡는다.
- 비기능 품질. 성능, 보안, 접근성, 사용성은 "몇 퍼센트의 줄을 실행했는가"와 무관하다(모듈 1 레슨 4의 비기능 테스트).
그렇다고 커버리지가 쓸모없다는 뜻은 아니다. 낮은 커버리지는 확실한 나쁜 신호다. 20%라면 80%의 코드는 한 번도 실행해보지 않은 것이니 위험 신호로 읽을 수 있다. 다만 높은 커버리지가 좋은 품질의 증거는 아니다. 커버리지의 종류(구문·분기·조건)와 실무에서 목표치를 어떻게 잡는지는 모듈 10에서 자세히 다룬다.
그럼 언제 멈추는가 — 테스트 종료 조건 맛보기
"다 볼 수 없다"의 자연스러운 다음 질문은 "그럼 언제 멈추나" 다. 실무에서는 이걸 감이 아니라 테스트 종료 조건(exit criteria) 으로 미리 정해둔다. 상세한 설계는 모듈 4에서 다루고, 여기서는 어떤 신호들을 보는지만 먼저 익혀두자.
| 신호 | 무엇을 보는가 | 이것만 보면 생기는 함정 |
|---|---|---|
| 계획 범위 소화 | 계획한 케이스의 실행률과 통과율 | 케이스 자체가 얕으면 숫자만 채워진다 |
| 결함 발견율 감소 | 시간당 새로 나오는 결함 수가 꺾였는가 | 살충제 패러독스일 수도 있다 — 같은 곳만 보고 있어서 안 나오는 것일 수 있다 |
| 잔여 결함 심각도 | 미해결 결함 중 Critical·High가 남아 있는가 | 심각도 판정이 낙관적으로 기울 수 있다 |
| 리스크 커버 | 사전에 뽑은 고위험 항목이 모두 다뤄졌는가 | 리스크 목록 자체가 빠뜨린 항목은 보이지 않는다 |
| 시간·비용 예산 | 남은 일정과 인력 | 이것 하나만이 기준이면 그건 종료 조건이 아니라 그냥 마감이다 |
현실에서는 마지막 항목이 사실상 결정권을 갖는 경우가 많다. 그래서 QA에게 중요한 건 "시간이 없어서 멈춘다"를 막는 게 아니라, 멈추는 시점에 "여기까지 봤고 여기는 못 봤다"를 명시적으로 남기는 것이다. 남긴 기록이 있으면 그건 팀의 의사결정이 되고, 없으면 나중에 QA 개인의 실수가 된다.
이 원칙이 뜻하지 않는 것
"완전한 테스트는 불가능하다"는 문장은 자주 잘못 인용된다. 오해와 정확한 해석을 나란히 놓으면 이렇다.
| 잘못된 결론 | 정확한 결론 |
|---|---|
| 어차피 다 못 보니 대충 본다 | 다 못 보니 어디를 볼지 근거를 갖고 고른다 |
| 버그는 원래 남는 거니 QA 책임은 없다 | 무엇이 남았는지 말할 수 있어야 책임을 팀이 나눠 진다 |
| 테스트 통과 = 품질 보증 | 테스트 통과 = 시도한 조건에서 문제가 안 보였다 |
| 커버리지 100%면 끝난 것 | 커버리지는 도달의 지표일 뿐이다 |
| 자동화를 늘리면 언젠가 전부 볼 수 있다 | 자동화는 표본을 빠르게 반복할 뿐, 표본을 전수로 만들지 않는다 |
| 케이스를 많이 만들수록 좋다 | 잘 고른 200건이 아무렇게나 만든 2,000건보다 낫다 |
무엇이 "충분"인지는 맥락에 따라 완전히 다르다. 개인 블로그의 댓글 기능과 의료기기 소프트웨어의 투여량 계산은 요구되는 테스트 수준이 같을 수 없다. 그래서 이 원칙의 결론은 "적당히"가 아니라 "얼마나 볼지를 의도적으로, 근거를 갖고 정하라" 다. 그 결정 방법이 모듈 4(리스크 기반 테스트)의 주제이고, 표본을 실제로 뽑아내는 기법이 모듈 3(테스트 설계 기법)의 주제다. 이 레슨은 그 두 모듈이 왜 존재하는지를 설명하는 자리인 셈이다.
실무에서 왜 필요한가
"버그 없이 나가는 거죠?"에 답해야 한다. 기획자나 경영진이 이렇게 묻는 상황은 드물지 않다. 세 가지 답을 비교해보자.
| 답변 | 무엇이 문제인가 |
|---|---|
| "네, 문제없습니다." | 지킬 수 없는 약속이다. 한 건이라도 터지면 QA 전체의 신뢰가 깎인다 |
| "버그가 없다고는 말씀드릴 수 없습니다." | 사실이지만 정보가 0이다. QA가 책임을 피하는 것처럼 들린다 |
| "핵심 결제 흐름은 경계값까지 촘촘히 봤고 발견 결함은 모두 수정·재확인했습니다. 다만 모든 조합을 볼 수는 없어 리스크가 낮다고 판단한 영역(예: 관리자 설정 화면)은 얕게 봤습니다. 이 부분이 남은 리스크입니다." | 문제없음 — 무엇을 봤고, 무엇을 왜 안 봤는지가 다 들어 있다 |
세 번째 답의 핵심은 마지막 문장이다. "무엇을 왜 안 봤는지"까지 말할 수 있는 것이 신뢰를 만든다. 안 본 영역을 숨기면 그건 QA 혼자 진 리스크가 되고, 밝히면 팀이 함께 진 리스크가 된다.
테스트 일정을 협상할 수 있게 된다. "시간이 부족합니다"는 협상이 되지 않는다. 반면 "지금 계획으로는 결제·로그인·주문 조회까지 봅니다. 이틀을 더 주시면 쿠폰 조합과 모바일 브라우저 2종을 추가로 볼 수 있습니다. 이틀이 없다면 그 두 영역이 남은 리스크입니다"는 협상이 된다. 표본을 고르는 축을 갖고 있으면 시간을 결과물의 언어로 환산해서 말할 수 있다.
릴리즈 후 사고가 났을 때 회고의 방향이 바뀐다. "왜 이걸 못 잡았냐"는 질문에 "죄송합니다"로 답하면 다음에 달라지는 게 없다. "이 케이스는 저희 표본 축 어디에도 걸리지 않았습니다 — 사용 빈도가 낮고 최근 변경도 없어서 우선순위가 낮았는데, 실제로는 영향도가 컸습니다. 리스크 평가에서 영향도를 과소평가한 것이 원인이라 판단합니다"로 답하면 표본 전략을 고치는 회고가 된다. 개인 책임 추궁이 아니라 시스템 개선으로 가는 길이 여기서 갈린다.
자동화 스위트를 관리하는 눈이 생긴다. 자동화 케이스 수와 통과율만 보고 있으면 앞의 사례 연구 같은 상황을 못 알아챈다. "최근 6개월간 이 스위트가 잡아낸 결함이 몇 건인가", "케이스의 평균 나이가 얼마인가"를 같이 보는 습관이 스테이지 4(자동화)에서 계속 쓰인다.
신입 QA의 소진을 막는다. 다 봐야 한다고 믿으면 끝없이 클릭하다 지치고, 정작 중요한 곳은 얕게 본다. "다 볼 수 없다"는 사실을 먼저 받아들여야 우선순위를 정하는 일에 에너지를 쓸 수 있다. 이건 태도의 문제가 아니라 직무 정의의 문제다.