개념
검증(Verification) vs 확인(Validation)
왜 비슷해 보이는 두 단어를 굳이 나누는지부터 짚자. 둘 다 "맞는지 본다" 는 뜻이지만, 무엇과 맞춰 보는지가 다르다. 하나는 적어 둔 문서 와 맞춰 보고, 다른 하나는 실제로 쓰는 사람의 목적 과 맞춰 본다. 문서대로 만들었는데도 쓸모가 없을 수 있기 때문에 두 가지를 따로 센다 — 레슨 1의 우산이 바로 그 이야기였다.
두 단어가 한국어로도 영어로도 헷갈리기 쉬운데, 질문 형태로 외우면 구분이 쉽다.
| 질문 | 확인 대상 | 시점 | |
|---|---|---|---|
| 검증 (Verification) | "우리가 제대로 만들고 있는가?" | 산출물이 명세·설계와 일치하는가 | 개발 각 단계마다 (문서, 설계, 코드) |
| 확인 (Validation) | "우리가 맞는 것을 만들고 있는가?" | 실제로 사용자 요구를 충족하는가 | 보통 동작하는 소프트웨어로, 더 나중에 |
두 단어의 구분은 ISTQB Foundation Level 실러버스 1장에 그대로 등장하는 정식 용어이기도 하다. 나중에 "V-모델" 같은 개발 생명주기 그림을 보게 되면, 그 그림의 왼쪽 절반은 대부분 검증을, 오른쪽 절반은 확인을 나타낸다는 걸 알아볼 수 있을 것이다 — 지금은 질문 두 개로 구분하는 것만 확실히 해두면 충분하다.
검증은 "명세대로 만들었는가"를 보고, 확인은 "그 명세 자체가 맞았는가, 실제로 쓸모 있는가"를 본다. 레슨 1의 품질 두 축(명세 충족 / 쓰임에 적합함)과 정확히 짝을 이룬다 — 검증은 첫 번째 축을, 확인은 두 번째 축을 담당한다.
쉬운 비유로 다시 보기 — 레시피와 손님
요리사가 레시피대로 정확히 요리를 만들었는지 확인하는 사람과, 그 요리를 먹은 손님이 맛있어하는지 확인하는 사람은 다른 질문을 던진다.
- 검증: "레시피에 소금 5g이라고 적혀 있는데, 정말 5g을 넣었는가?" — 주방장이 레시피와 완성된 요리를 대조한다.
- 확인: "그런데 이 손님은 원래 싱겁게 먹는 사람인데, 이 요리를 맛있어할까?" — 손님이 직접 먹어봐야만 알 수 있다.
레시피대로 완벽하게 만들었어도(검증 통과) 그 손님 입맛에는 너무 짤 수 있다(확인 실패). 반대로 레시피에 없는 향신료를 셰프가 감으로 살짝 더했는데 손님이 "이게 훨씬 낫다"고 할 수도 있다(검증 실패, 확인은 우연히 통과 — 이 경우 레시피를 고쳐야 한다는 신호다). 소프트웨어에서도 똑같은 일이 벌어진다 — "명세대로 정확히 만들었다"는 개발팀의 말과 "그런데 이걸 왜 이렇게 만들었어요?"라는 사용자의 반응이 동시에 참일 수 있다.
둘은 독립적으로 실패한다
이게 핵심이다. 네 가지 조합이 전부 실제로 일어난다.
| 검증 | 확인 | 무슨 상황인가 | 실무에서 |
|---|---|---|---|
| ✅ | ✅ | 명세대로 만들었고 사용자도 만족 | 목표 상태 |
| ✅ | ❌ | 명세대로 만들었는데 아무도 못 씀 | 가장 위험. 테스트는 전부 통과하는데 제품이 실패한다 |
| ❌ | ✅ | 명세와 다르게 만들었는데 오히려 자연스러움 | 드물다. 명세를 갱신하라는 신호 |
| ❌ | ❌ | 명세와도 다르고 쓸모도 없음 | 발견은 쉽다. 대개 빨리 잡힌다 |
두 번째 줄이 QA가 가장 경계해야 할 상태다. 모든 테스트 케이스가 초록불인데 제품이 실패한다. 테스트 케이스는 명세에서 나왔고, 그 명세가 틀렸기 때문이다. 이 상태는 테스트 케이스를 아무리 늘려도 절대 발견되지 않는다 — 실제 사용자를 봐야만 드러난다.
이게 왜 무서운가 하면, QA가 스스로의 성과를 잘못 판단하게 만들기 때문이다. "테스트 케이스 200개, 전부 통과, 결함 0건"이라는 보고는 근사해 보이지만, 그 200개가 전부 틀린 명세에서 나온 것이라면 이 숫자는 오히려 위험 신호를 가려버린다. 통과율은 검증의 결과일 뿐이지 확인의 결과가 아니다 — 이 둘을 섞어서 보고하면 "테스트를 열심히 했는데 왜 사용자는 화가 났나요?"라는, 가장 답하기 곤란한 질문을 받게 된다.
어떤 활동이 어느 쪽인가
표에 단위 테스트·통합 테스트 가 나온다. 각각 "부품 하나만 따로 돌려 보는 시험", "부품 두어 개를 맞물려 돌려 보는 시험" 이라고 알아 두면 지금은 충분하다. 다음 레슨 전체가 이 구분을 다룬다.
| 활동 | 검증 | 확인 |
|---|---|---|
| 코드 리뷰 | ✅ | |
| 단위·통합 테스트 | ✅ | |
| 명세 대비 추적표(traceability matrix) 점검 | ✅ | |
| 설계 문서 리뷰 | ✅ | |
| 사용자 인수 테스트(UAT) | ✅ | |
| 베타 테스트·사용성 테스트 | ✅ | |
| 실사용 데이터·이탈률 분석 | ✅ | |
| 프로토타입을 사용자에게 보여주기 | ✅ |
마지막 줄이 중요하다. 확인은 소프트웨어가 완성된 뒤에만 할 수 있는 게 아니다. 종이에 그린 화면, 클릭만 되는 프로토타입으로도 "이게 사용자가 원하는 게 맞나"를 물어볼 수 있다. 레슨 1의 비용 곡선을 떠올리면, 확인을 앞당길수록 싸다.
오류 → 결함 → 실패의 전파
세 단어를 QA에서는 엄격하게 구분한다.
- 오류(Error/Mistake): 사람이 저지른 실수. 요구사항을 잘못 이해했거나, 코드를 잘못 짰거나.
- 결함(Defect/Fault/Bug): 그 실수가 산출물(코드, 문서, 설계)에 남긴 흔적. 아직 실행되지 않았을 수도 있다.
- 실패(Failure): 그 결함이 특정 조건에서 실행되어 겉으로 드러난 오작동.
// 오류: 개발자가 "빈 배열도 유효한 입력"이라는 걸 놓쳤다 (사람의 실수)
function getDiscountRate(items) {
// 결함: items가 빈 배열일 때를 고려하지 않은 코드가 여기 남아 있다
return items[0].price > 100000 ? 0.1 : 0.05;
}
getDiscountRate([{ price: 150000 }]); // 0.1 — 정상 동작, 실패가 드러나지 않는다
getDiscountRate([]); // TypeError — 여기서 실패가 발생한다이 코드는 결함이 있어도 대부분의 입력에서는 실패하지 않는다. 빈 배열이 들어오는 경로를 실행해야만 실패로 드러난다. 그래서 "지금까지 문제가 없었다"는 "결함이 없다"는 뜻이 아니라 "그 결함을 건드리는 조건을 아직 실행해보지 않았다" 는 뜻일 수 있다.
결함이 실패가 되려면 세 가지가 모두 필요하다
결함이 있다고 항상 실패가 보이는 건 아니다. 세 조건이 다 맞아야 한다.
- 도달(Reachability) — 그 코드 줄이 실행되어야 한다
- 감염(Infection) — 실행 결과로 프로그램 내부 상태가 잘못돼야 한다
- 전파(Propagation) — 그 잘못된 상태가 밖으로 관찰 가능한 형태로 나와야 한다
세 번째가 특히 함정이다. 내부적으로 잘못 계산됐는데 화면에 표시할 때 반올림되어 같아 보이는 경우, 테스트는 통과한다. 결함은 그대로 남아 있고 조건이 조금만 달라지면 터진다.
// 결함: 세금을 더해야 하는데 빼고 있다
function total(price) {
return price - price * 0.1; // 잘못된 부호
}
// price = 0 이면? → 0 (정답도 0) — 도달했지만 "감염"이 관찰되지 않는다
// price = 10000 이면? → 9000 (정답 11000) — 여기서 비로소 실패가 보인다"0으로 테스트했더니 통과했다"가 왜 위험한지가 여기 있다. 모듈 3에서 배울 동등분할·경계값이 "실패를 드러나게 만드는 입력을 고르는 기술"인 이유다.
실패가 안 보이는 결함은 안전한가
아니다. 두 가지 이유로 위험하다.
- 조건이 바뀌면 갑자기 터진다. 위
getDiscountRate예시에서, 지금까지는 장바구니가 빈 채로 이 함수를 부르는 경로가 없었을 뿐이다. 다음 스프린트에 "빈 장바구니에서도 예상 할인율 미리보기" 기능이 추가되는 순간 실패한다. 코드는 안 바뀌었는데 실패가 시작된다. - 조용히 잘못된 데이터를 만든다. 화면에는 안 보여도 DB에는 잘못된 값이 쌓일 수 있다. 레슨 1에서 본 것처럼, 데이터 오염은 코드 수정보다 훨씬 비싼 복구를 부른다.
이 구분이 결함 리포트를 바꾼다
전파 경로를 알면 리포트에 무엇을 써야 하는지가 분명해진다.
| 빈약한 리포트 | 전파 경로를 아는 리포트 |
|---|---|
| "할인율 계산이 에러남" (실패만 기술) | "빈 장바구니 상태에서 할인율 미리보기를 열면(조건) getDiscountRate가 items[0]에 접근하며(결함) TypeError 발생(실패)" |
오른쪽은 개발자가 재현 절차를 다시 물을 필요가 없다. "어떤 조건이 그 결함을 건드렸는가"를 적었기 때문이다. 결함 리포트 작성법은 모듈 6에서 자세히 다룬다.
근본 원인은 결함이 아니라 오류 쪽에 있다
결함을 고치면 그 실패는 사라진다. 하지만 같은 오류(사람의 실수 패턴)가 남아 있으면 비슷한 결함이 다른 곳에서 또 생긴다.
이번 결함:
getDiscountRate가 빈 배열을 고려하지 않음 그 뒤의 오류: "목록은 항상 최소 1개"라는 검증되지 않은 가정을 팀이 공유하고 있음 → 그렇다면 다른 함수들도 같은 가정을 하고 있을 가능성이 높다
이 관점이 QA를 "버그 잡는 사람"에서 "같은 종류의 버그가 다시 안 생기게 하는 사람"으로 바꾼다. 결함 하나를 발견했을 때 "이 실수가 다른 곳에도 있지 않을까?" 를 함께 묻는 습관이 여기서 나온다 — 레슨 6에서 다룰 QA(프로세스)와 QC(결과물)의 차이와도 닿아 있다.
사례 연구 — 할인율이 어쩌다 마이너스가 됐나
이 레슨에서 배운 개념들이 실제로 어떻게 이어지는지, 하나의 흐름으로 짚어본다.
제보: "가끔 주문 금액이 원래보다 더 나온다"는 CS 문의가 몇 건 들어왔다.
1단계 — 검증부터 시작한다. 할인율 계산 명세를 열어보니 "10만원 이상 구매 시 10% 할인"이라고 적혀 있고, 코드도 그 로직 그대로다. 명세와 코드는 일치한다 — 검증은 통과. 하지만 검증 통과가 "문제없다"는 뜻이 아니라는 걸 이제는 안다.
2단계 — 오류 → 결함의 흔적을 찾는다. 코드를 더 보니, 쿠폰과 할인율이 함께 적용될 때 할인율을 두 번 빼는 구간이 있었다. 개발자는 "쿠폰과 등급 할인은 동시에 안 쓴다"고 가정하고 짰는데(오류), 실제로는 두 할인이 겹치는 경우가 있었다(결함). 평소엔 안 겹쳐서 티가 안 났을 뿐이다(도달했지만 감염이 드물게만 일어남 — 앞서 배운 개념 그대로다).
3단계 — 실패 조건을 좁힌다. "쿠폰 + 등급 할인이 동시에 적용되는 주문"이라는 조건에서만 재현된다는 걸 확인했다. 이게 바로 "어떤 조건이 결함을 건드렸는가"이고, 이걸 리포트에 적으면 개발자가 되묻지 않는다.
4단계 — 확인까지 가본다. 코드를 고친 뒤, 실제로 쿠폰과 등급 할인을 함께 쓰는 사용자 흐름을 처음부터 다시 밟아본다. 계산만 맞는지가 아니라, 화면에 표시되는 최종 금액이 사용자 입장에서 납득 가는 숫자인지까지 본다 — 여기가 확인이다.
5단계 — 오류 수준으로 올라간다. "동시에 안 쓴다"는 검증되지 않은 가정이 이 결함의 진짜 원인이었다. 그렇다면 이 코드베이스의 다른 할인·적립 로직에도 같은 가정이 깔려 있지 않을까? 이 질문을 던지는 게 이 레슨의 마지막 절에서 말한 "근본 원인은 결함이 아니라 오류 쪽에 있다"를 실제로 적용하는 것이다.
한 건의 CS 문의가 검증 → 결함 추적 → 조건 좁히기 → 확인 → 재발 방지 질문까지 이어지는 흐름을 보여준다. 이 다섯 단계는 앞으로 배울 결함 리포트 작성(모듈 6), 탐색적 테스팅(모듈 5)에서도 계속 반복해서 쓰이는, QA 업무의 가장 기본적인 골격이다.
실무에서 왜 필요한가
실무에서 가장 흔한 오해는 "명세대로 만들었으니 QA 책임은 끝났다"는 생각이다. 하지만 명세 자체가 틀렸다면(확인 실패), 그건 QA가 검증만 하고 확인을 건너뛰었기 때문일 수 있다. 예를 들어 결제 폼이 요구사항 문서와 한 글자도 다르지 않게 구현됐지만(검증 통과), 실제 사용자 테스트에서 결제 단계가 너무 복잡해 다들 중간에 이탈한다면(확인 실패), 그 결함은 테스트 케이스로는 절대 못 잡는다 — 실제 사용자 흐름을 봐야 잡힌다.
QA가 확인(Validation)에 기여하는 현실적인 방법은 이렇다. 사용자 리서치를 직접 하지 않더라도 할 수 있는 일이 많다.
- 요구사항 리뷰에서 "이 흐름을 처음 보는 사람이 이해할까?"를 묻기
- 실제 사용 데이터(이탈 지점, 오류 로그, CS 문의 내용)를 정기적으로 보기
- 테스트할 때 명세를 잠시 덮고 사용자처럼 써보기 — 스크립트 밖을 보는 이 활동이 모듈 5의 탐색적 테스팅이다
- 베타·UAT에서 나온 피드백을 "요구사항 대비 차이"로 번역해 전달하기
또한 "결함이 있어도 실패가 안 보이면 괜찮다"는 생각도 위험하다. 위 코드 예시처럼, 결함은 그대로 남아있다가 몇 달 뒤 특정 이벤트(예: 프로모션으로 빈 장바구니 관련 로직이 새로 실행되는 경로)에서 갑자기 실패로 터지는 경우가 실무에서 자주 있다. 그때는 "왜 갑자기?"라는 질문이 나오는데, 답은 "갑자기가 아니라 원래 있었다" 이다.
신입 QA가 특히 조심할 것 — "제 담당은 검증까지입니다"라는 선 긋기. 실무에서 검증과 확인이 조직적으로 완전히 분리된 경우는 드물다. 신입일수록 검증(명세 대조)에 먼저 익숙해지고, 확인은 "시니어나 PM이 하는 일"로 여기기 쉽다. 하지만 위 목록처럼 확인에 기여하는 방법은 리서치 전문가가 아니어도 할 수 있는 것들이다. 오히려 매일 코드와 화면을 들여다보는 사람이 "이거 좀 이상한데"를 가장 먼저 느낄 위치에 있다. 이 감각을 "내 일이 아니다"로 넘기지 않는 것이 검증과 확인을 함께 챙기는 QA와, 체크리스트만 채우는 QA를 가른다.