개념
레벨과 유형은 서로 다른 축이다
레슨 3에서 배운 테스트 레벨은 "어느 범위를 함께 실행해서" 확인하는가였다. 단위·통합·시스템·인수. 이번 레슨의 테스트 유형은 "무엇을" 확인하는가다.
둘은 같은 줄에 놓고 고르는 선택지가 아니다. 동시에 정해지는 두 개의 좌표다. "결제 화면을 테스트한다"는 말만으로는 아무것도 정해지지 않는다. 실무의 테스트 계획서 한 줄은 항상 이 두 좌표를 같이 갖는다.
| 기능 유형 | 비기능 유형 | |
|---|---|---|
| 단위 레벨 | 할인 계산 함수가 정확한 금액을 돌려주는가 | 그 함수가 10만 건을 처리하는 데 몇 초 걸리는가 |
| 시스템 레벨 | 로그인부터 결제까지 흐름이 끝나는가 | 동시 접속 1,000명에서 결제가 5초 안에 끝나는가 |
표의 네 칸이 전부 서로 다른 테스트다. 레벨을 골랐다고 유형이 정해지지 않고, 유형을 골랐다고 레벨이 정해지지 않는다. 이 감각을 잡는 것이 이 레슨의 첫 목표다.
말버릇으로 굳혀두면 편하다 — 테스트를 하나 설계할 때마다 속으로 두 문장을 완성해본다. "어느 범위에서 돌리는가", "무엇을 확인하는가."
기능 테스트 — 무엇을 하는가
기능 테스트(Functional Testing) 는 소프트웨어가 하기로 되어 있는 일을 하는가를 본다. 판단 기준은 요구사항·기획서·사용자 스토리에 적힌 동작 그 자체다.
- 로그인 버튼을 누르면 로그인이 되는가
- 3만원 이상 주문에 배송비가 0원으로 계산되는가
- 잘못된 비밀번호를 5회 입력하면 계정이 잠기는가
- 품절 상품은 장바구니에 담기지 않는가
기능 테스트의 결과는 대체로 맞다/틀리다로 딱 떨어진다. 기대 결과가 "9,000원"인데 "9,000원"이 나오면 통과, "10,000원"이 나오면 실패다. 여기에는 해석의 여지가 거의 없다.
QA가 하는 일의 가장 큰 덩어리가 여기이고, 스테이지 2의 테스트 설계 기법(모듈 3의 동등분할·경계값·결정표 같은 것)은 대부분 기능 테스트 케이스를 효율적으로 뽑는 방법이다.
비기능 테스트 — 얼마나 잘 하는가
비기능 테스트(Non-functional Testing) 는 같은 기능을 놓고 얼마나 잘 하는가를 묻는다. 기능은 정확히 동작하는데도 쓸 수 없는 제품이 되는 지점이 여기다.
레슨 1에서 "품질은 하나의 값이 아니라 여러 축의 묶음"이라고 했던 것을 떠올려보자. 그때 이름만 스쳤던 ISO/IEC 25010의 갈래들이 바로 비기능 테스트의 목록이다.
핵심 차이는 합격선이 숫자로 정해져야 한다는 점이다. 기능 테스트는 "된다/안 된다"로 끝나지만, 비기능 테스트는 "3초 안에 응답한다"처럼 기준선이 없으면 판정 자체가 불가능하다. "좀 느린 것 같아요"는 결함 리포트가 아니라 감상이다.
비기능의 갈래 — 맛보기
각 갈래는 스테이지 5(모듈 16 비기능 테스트 종합)에서 도구와 함께 제대로 다룬다. 지금은 "이런 질문들이 따로 있다" 는 목록만 머리에 넣어두면 충분하다.
| 갈래 | 한 줄로 하면 이런 질문 | 실패했을 때 보이는 모습 |
|---|---|---|
| 성능(Performance) | 평소 조건에서 얼마나 빨리 응답하는가 | 검색 결과가 8초 뒤에 뜬다 |
| 부하(Load) | 예상 최대 사용자 수를 견디는가 | 티켓 오픈 10분간 접속이 안 된다 |
| 스트레스(Stress) | 한계를 넘겼을 때 어떻게 무너지고 어떻게 복구되는가 | 트래픽이 빠진 뒤에도 서버가 안 돌아온다 |
| 보안(Security) | 권한 없는 사람이 못 하게 막혀 있는가 | 남의 주문번호를 URL에 넣으면 남의 주소가 보인다 |
| 사용성(Usability) | 처음 쓰는 사람이 헤매지 않고 끝낼 수 있는가 | 기능은 있는데 아무도 못 찾는다 |
| 신뢰성(Reliability) | 오래 돌려도 죽지 않는가, 죽으면 되살아나는가 | 3일째 되는 날마다 메모리가 차서 재시작된다 |
| 호환성(Compatibility) | 다른 브라우저·기기·OS 버전에서도 되는가 | iOS 16에서만 결제 버튼이 안 눌린다 |
| 접근성(Accessibility) | 스크린리더·키보드만으로도 쓸 수 있는가 | 키보드 탭으로 결제까지 못 간다 |
스크린리더 는 화면의 글자를 소리로 읽어 주는 프로그램이다. 눈으로 보지 않고 소리와 키보드만으로 결제까지 갈 수 있는지가 접근성 시험의 대표 항목이다.
이 표에서 지금 가져갈 것은 딱 하나다. 오른쪽 칸의 사례들은 기능 테스트를 아무리 꼼꼼히 해도 하나도 안 걸린다. 기능 테스트는 "혼자서, 평상시 조건에서, 최신 크롬으로, 본인 계정으로" 확인하기 때문이다.
기능이 통과해도 비기능이 무너지면 실제로 무슨 일이 일어나는가
"기능은 되는데 느리다"는 말은 추상적으로 들린다. 구체적인 결과로 바꿔보면 왜 이게 별도 범주여야 하는지가 분명해진다.
- 사용자가 조용히 떠난다. 결제 버튼을 눌렀는데 8초 동안 아무 반응이 없으면 사용자는 버튼을 다시 누르거나 창을 닫는다. 이 사람은 버그 리포트를 쓰지 않는다. 기능 결함은 CS로 들어오지만, 성능 문제는 이탈률 그래프에만 남는다. 그래서 더 늦게 발견된다.
- 중복 요청이 기능 결함을 만든다. 느려서 두 번 누른 결제가 두 건으로 들어가면, 그때부터는 성능 문제가 아니라 중복 결제라는 기능 결함이 된다. 비기능 문제가 기능 문제로 번지는 전형적인 경로다.
- SLA 위반은 계약과 돈의 문제가 된다. B2B 서비스는 "월 가용성 99.9%", "응답 99퍼센타일 2초 이내" 같은 수치를 계약서에 적는다. 이걸 넘기면 배상·감액 조항이 걸린다. 이때 QA에게 오는 질문은 "왜 이걸 테스트 안 했나요"다.
- 보안 결함은 되돌릴 수 없다. 기능 버그는 고치면 끝이지만, 유출된 개인정보는 회수할 수 없다. 신고 의무와 과징금이 따라붙는다.
- 뒤늦게 고치려면 구조를 바꿔야 한다. 성능 문제의 원인이 화면 코드가 아니라 데이터 구조나 아키텍처에 있는 경우가 많다. 레슨 1의 비용 곡선이 비기능 결함에서 특히 가파른 이유다.
그래서 실무에서 자주 쓰는 문장이 있다. "기능 테스트는 제품이 존재하는지를 확인하고, 비기능 테스트는 그 제품을 쓸 수 있는지를 확인한다."
확인 테스트 vs 회귀 테스트
여기서 축이 한 번 더 바뀐다. 기능/비기능이 "무엇을 보는가"였다면, 확인/회귀는 "왜 지금 이걸 다시 돌리는가" 다. 둘 다 결함을 수정한 뒤에 하는 활동이고, 이름이 비슷해서 실무에서 가장 자주 섞인다.
| 확인 테스트 (Confirmation / Re-testing) | 회귀 테스트 (Regression) | |
|---|---|---|
| 질문 | 이 결함이 진짜 고쳐졌는가? | 이 수정이 다른 곳을 망가뜨리지 않았는가? |
| 대상 | 원래 실패했던 그 케이스 | 수정과 관련 있을 수 있는 주변 기능 |
| 케이스 출처 | 결함 리포트의 재현 절차 그대로 | 기존 테스트 스위트에서 골라낸 것 |
스위트(suite) 는 테스트 케이스를 한 묶음으로 모아 둔 것이다. "회귀 스위트" 는
"바꿀 때마다 돌리는 묶음" 을 가리킨다.
| 개수 | 보통 13개 | 보통 수십수백 개 |
| 자동화 적합도 | 낮음 (일회성이 많음) | 매우 높음 (반복 실행) |
| 놓치면 생기는 일 | 안 고쳐진 버그를 고쳐졌다고 착각 | 새 버그(회귀 결함)를 릴리즈에 포함 |
확인 테스트에는 규칙이 하나 있다. 결함 리포트에 적힌 재현 절차를 한 글자도 바꾸지 않고 그대로 실행해야 한다. 다른 계정, 다른 금액, 다른 순서로 해보고 "되네요"라고 하면 그건 확인 테스트가 아니다. 원래 실패했던 그 조건이 통과해야 비로소 "고쳐졌다"고 말할 수 있다. 레슨 2의 도달·감염·전파 세 조건을 떠올려보자 — 조건을 살짝만 바꿔도 결함에 도달조차 못 할 수 있다.
코드로 보는 확인과 회귀
말로만 하면 여전히 흐릿하다. 실제 수정 하나를 놓고 보자. "정액 쿠폰이 적용되지 않는다"는 결함을 개발자가 이렇게 고쳤다.
function calculateDiscount(total, coupon) {
- return total * coupon.rate
+ if (coupon.type === "FIXED") {
+ return coupon.amount
+ }
+ return total * coupon.rate
}확인 테스트는 이 함수가 원래 실패했던 그 입력에서 이제 맞는 값을 내는지 본다. 결함 리포트에 적힌 그대로 — 3,000원 정액 쿠폰, 총액 20,000원.
| 입력 | 기대 | 확인할 것 |
|---|---|---|
type: "FIXED", amount: 3000 | 3,000원 할인 | 리포트의 그 케이스가 통과하는가 |
회귀 테스트는 다른 질문을 한다. 이 함수를 부르는 다른 곳들이 여전히 멀쩡한가. 바뀐 것은 이 함수 하나지만, 이 함수의 결과를 쓰는 코드는 여러 개다.
// A) 결제 화면 — 최종 결제 금액
finalPrice = total - calculateDiscount(total, coupon)
// B) 상품 목록 — "이 쿠폰 쓰면 얼마?" 미리보기 배지
badgeText = `${calculateDiscount(previewTotal, coupon)}원 할인`
// C) 정산 배치 — 매출에서 할인액을 빼서 리포트 생성
netRevenue = grossRevenue - sumOf(calculateDiscount(...))이제 회귀 질문이 구체적으로 나온다.
- 정액 쿠폰이 총액보다 클 때 — 5,000원 쿠폰에 3,000원 주문이면 A는 음수 결제가
되는가? (수정 전에는
rate곱셈이라 절대 총액을 넘지 못했다. 수정이 새로운 가능성을 열었다.) - B의 배지는
calculateDiscount가 비율일 때와 정액일 때 같은 문구를 쓰는가? "10% 할인"이라고 표시하던 자리에 "3000원 할인"이 들어가면 문구가 깨지지 않는가? - C의 정산 금액이 A의 결제 금액과 여전히 일치하는가? 둘이 어긋나면 회계가 안 맞는다.
- 기존 정률 쿠폰은 그대로 동작하는가? (
type이"PERCENT"가 아니라 비어 있는 옛날 데이터가 있다면 어느 갈래로 가는가?)
마지막 항목이 회귀 테스트의 전형이다. 고친 것이 아니라 안 고친 것을 확인한다.
정률 쿠폰은 아무도 건드리지 않았지만, 같은 함수 안에 새 if가 생겼으므로 다시
확인해야 한다.
회귀 범위를 어떻게 좁히는가 — 영향 분석의 감각
여기서 실무의 진짜 난제가 나온다. 회귀 테스트를 전부 돌리면 안전하지만 느리고, 안 돌리면 빠르지만 사고가 난다. 그래서 필요한 것이 영향 분석(Impact Analysis) — "이 변경이 어디까지 번질 수 있는가"를 좁히는 작업이다.
거창한 도구 없이도 쓸 수 있는 휴리스틱이 있다. 아래 순서대로 물어보면 대부분의 현실적인 회귀 범위가 나온다.
| 단계 | 질문 | 어떻게 알아내는가 |
|---|---|---|
| 1 | 무엇이 바뀌었나? | PR의 변경 파일 목록을 본다 (모듈 9의 diff 읽기) |
| 2 | 그 파일을 누가 부르는가? | 저장소에서 함수명·파일명을 전체 검색한다 |
| 3 | 그 호출자를 또 누가 부르는가? | 한 단계만 더 따라간다. 보통 2단계면 충분하다 |
| 4 | 같은 데이터를 읽는 다른 기능은? | 같은 DB 테이블·같은 API를 쓰는 화면들 |
| 5 | 과거에 여기서 터진 적 있나? | 결함 이력에서 같은 모듈 검색 |
2번이 가장 강력하면서 가장 안 쓰이는 방법이다. 코드 저장소에서 calculateDiscount를
검색하면 그 함수를 쓰는 파일이 전부 나온다. 개발자에게 "이거 어디서 써요?"라고
묻기 전에 30초면 스스로 알 수 있다. 개발자 도구를 쓸 줄 알면 여기에 하나 더 붙는다 —
Network 탭에서 어떤 API가 그 값을 실어 나르는지 보면 화면 쪽 영향 범위도 잡힌다.
여기에 위험도 가중치를 얹는다. 영향 범위 안에 있더라도 전부 같은 무게가 아니다.
- 돈이 오가는 경로(결제·정산·환불)는 영향 가능성이 낮아도 넣는다
- 되돌릴 수 없는 동작(발송된 알림, 확정된 주문)도 넣는다
- 최근 자주 깨진 영역은 확률이 실제로 높다
- 반대로 사용률이 낮고 되돌리기 쉬운 화면은 이번엔 뺄 수 있다
이 판단은 모듈 2의 요구사항 변경 영향 분석, 모듈 4의 리스크 기반 테스트와 같은 근육이다. 그리고 회귀 스위트를 자동화해두면 이 고민의 상당 부분이 사라진다 — 1번부터 5번까지 따지는 대신 그냥 다 돌리면 되기 때문이다. 이게 모듈 11(자동화 전략)과 모듈 14(CI/CD)에서 회귀 테스트를 자동화 1순위로 꼽는 이유다.
사례 연구 — 30분 핫픽스가 이틀 뒤에 청구서로 돌아오다
앞의 조각들을 하나의 흐름으로 이어보자. 실제로 흔한 순서다.
화요일 14:00 — 제보. CS에 "정액 쿠폰이 적용 안 된다"는 문의가 20건 쌓였다. 매출에 직접 영향이 있으니 핫픽스로 결정된다.
14:30 — 수정. 개발자가 위의 diff대로 calculateDiscount에 if (coupon.type === "FIXED")
갈래를 추가한다. 세 줄짜리 변경이다.
15:00 — 확인 테스트. QA가 리포트의 재현 절차대로 3,000원 쿠폰을 20,000원 주문에 적용한다. 17,000원. 통과. 리포트에 "수정 확인 완료"를 남긴다.
15:10 — 생략된 판단. "세 줄 바꿨는데 뭐가 문제겠어요"라는 말이 나온다. 회귀 테스트는 다음 정기 릴리즈에 몰아서 하기로 하고 배포한다. 여기까지 40분. 팀은 이걸 성공적인 대응이라고 기억한다.
목요일 09:00 — 이상 신호. 정산 담당자가 "어제 매출 리포트의 할인 합계가 결제 합계와 12만원 안 맞는다"고 슬랙에 올린다. 아무도 결제 버그라고 생각하지 않는다. 정산 시스템 문제로 보였기 때문이다.
목요일 11:00 — 두 번째 신호. 고객이 "0원 결제가 됐는데 상품이 왔다"고 문의한다. 5,000원 정액 쿠폰을 3,000원짜리 소액 주문에 쓴 것이다. 수정 전에는 비율 곱셈이라 할인액이 절대 총액을 넘을 수 없었다. 정액 갈래가 그 보호막을 없앴다.
목요일 14:00 — 원인 확정. 화요일의 세 줄이 원인으로 밝혀진다. 그동안 이틀치 주문이 쌓였다. 이제 할 일은 코드 수정 하나가 아니다.
- 이틀간 잘못 결제된 주문을 찾아내서 개별 보정
- 정산 리포트 재생성
- 영향받은 고객에게 안내
- 원인 설명과 재발 방지책 보고
총 비용: 화요일에 회귀 테스트로 15분 쓰지 않아 목요일에 이틀 + 여러 팀의 시간을 썼다. 레슨 1의 결함 발견 시점별 비용 곡선이 정확히 이 모양이다. 그리고 이 사고는 확인 테스트를 안 해서가 아니라 확인 테스트만 해서 생겼다.
한 가지 더 짚을 것이 있다. QA가 15:10에 던졌어야 할 질문은 "회귀 테스트 해도 되나요?"가 아니다. 그건 시간을 달라는 요청으로 들려서 거절되기 쉽다. 이렇게 물어야 한다 — "이 함수를 쓰는 데가 결제 말고 두 군데 더 있는데, 정액이면 할인액이 총액을 넘을 수도 있습니다. 15분이면 이 세 가지는 확인됩니다." 범위와 시간을 함께 제시하면 협상이 가능해진다.
회귀 테스트 케이스는 어디서 오는가
회귀 테스트를 하라고 하면 "그럼 케이스를 새로 만들어야 하나?"라고 생각하기 쉽다. 아니다. 회귀 테스트 케이스는 이미 있는 것 중에서 고른다. 주된 출처는 셋이다.
- 기존 기능 테스트 케이스 — 예전에 만들어 통과시킨 것들
- 과거 결함에서 나온 케이스 — 한 번 터진 곳은 또 터진다. 결함을 고칠 때마다 그 케이스를 회귀 스위트에 넣는 습관이 가장 값싼 보험이다
- 핵심 흐름(Critical Path) — 로그인, 검색, 장바구니, 결제처럼 깨지면 서비스가 멈추는 경로. 변경 내용과 무관하게 항상 포함한다
그리고 회귀 스위트는 계속 커지기만 하면 결국 아무도 안 돌린다. 주기적으로 덜어내야 한다. 없어진 기능의 케이스, 몇 년째 한 번도 실패한 적 없으면서 실행에 오래 걸리는 케이스는 후보다. 테스트 스위트도 유지보수 대상이라는 감각은 모듈 7(테스트 문서·프로세스 개선)에서 이어진다.
스모크 테스트는 어디에 들어가는가
현장에서 자주 듣는 두 단어를 여기 끼워두면 지도가 완성된다.
| 이름 | 언제 | 무엇을 |
|---|---|---|
| 스모크(Smoke) | 새 빌드를 받자마자 | 아예 테스트를 시작할 수 있는 상태인지. 로그인·주요 화면 진입 정도. 실패하면 즉시 반려 |
| 새니티(Sanity) | 작은 수정 뒤 | 그 기능 주변만 빠르게. 좁고 깊은 회귀의 축소판 |
| 회귀(Regression) | 릴리즈 전 | 영향 범위 + 핵심 흐름 전반 |
스모크와 새니티는 별개의 유형이라기보다 회귀 테스트를 시간 예산에 맞춰 잘라낸 크기들이라고 이해하면 된다. 팀마다 정의가 조금씩 다르니, 새 팀에 가면 "여기서 스모크는 뭘 도는 걸 말해요?"라고 한 번 물어보는 편이 낫다.
실무에서 왜 필요한가
"고쳤어요"와 "괜찮아요"는 다른 말이다. 개발자가 "고쳤어요"라고 할 때 그건 확인 테스트의 답이다. 릴리즈해도 되느냐는 질문의 답은 회귀 테스트에서만 나온다. 이 둘을 구분해서 말할 수 있는 것만으로 QA의 보고가 달라진다 — "수정은 확인했고, 영향 범위 세 곳 중 두 곳까지 봤습니다. 정산 쪽은 아직입니다"는 문장은 팀이 의사결정할 수 있는 정보다. "테스트 끝났어요"는 아니다.
핫픽스는 리스크가 가장 높은 배포다. 급하다는 이유로 절차가 줄고, 코드 리뷰가 얕아지고, 회귀가 생략된다. 정작 급한 상황일수록 사람은 더 실수한다. 위 사례처럼 "버그 하나 고칠 때마다 버그 하나가 새로 생긴다"는 평판은 팀의 배포 속도를 오히려 장기적으로 떨어뜨린다. 핫픽스일수록 범위를 좁히되 0으로 만들지 않는 것 — 이게 실무 QA의 판단력이다.
반대 방향의 실패도 흔하다. 매번 전체 회귀를 고집하면 릴리즈 주기가 늘어지고, 결국 팀이 QA를 우회하기 시작한다. "이번 건은 급해서 그냥 나갑니다"가 반복되면 프로세스 자체가 무력해진다. 회귀 범위를 근거를 대고 좁힐 수 있는 사람이 신뢰를 얻는다. 근거는 감이 아니라 영향 분석에서 나온다.
비기능을 나중으로 미루면 구조를 다시 짜야 한다. "일단 되게 만들고 성능은 나중에"는 거의 항상 비싸게 끝난다. 성능·보안의 원인은 화면이 아니라 데이터 구조와 아키텍처에 있는 경우가 많고, 그때는 이미 그 위에 다른 기능들이 얹혀 있다. QA가 기획 단계에서 할 수 있는 가장 값싼 기여는 "이 요구사항의 합격선은 숫자로 몇인가요?" 를 묻는 것이다 — 응답 시간, 동시 사용자 수, 지원 브라우저 범위. 답이 없으면 그건 아직 테스트 가능한 요구사항이 아니다(모듈 2에서 다룬다).
유형 구분은 회의에서 바로 쓰인다. "결제 테스트 했어요?"라는 질문에 유형 어휘가 있으면 정확히 답할 수 있다. "기능은 시스템 레벨까지 다 봤고, 부하는 아직 안 봤습니다. 쿠폰 수정 건 확인 테스트는 끝났고 회귀는 결제·정산까지 했습니다." 이 한 문장이 QA의 커버리지를 팀에 그대로 전달한다.