배포 때마다 가슴이 조마조마합니다. "이거 올리면 다른 데 안 깨질까요?" 아무도 확신 못 합니다. 지난번 결제 버그를 고쳤는데 한 달 뒤 똑같은 버그가 다시 났습니다. 한편 QA팀은 "테스트 커버리지 80% 달성!"을 자랑하는데, 정작 결제 핵심 흐름은 한 번도 단언(assert) 없이 '실행만' 되고 있었습니다. 테스트는 '품질을 비싸게 사는 보험'이 아니라, 변경을 두려움 없이 자주 할 수 있게 하는 속도의 기반입니다. 단, 잘못 쌓으면 느리고 거짓 신뢰만 줍니다.
- 1테스트 피라미드(단위>통합>E2E)의 비율과 이유를 설명할 수 있다
- 2커버리지 숫자의 의미와 함정(실행≠검증)을 설명할 수 있다
- 3스모크·회귀 테스트가 각각 무엇을 막는지 구분할 수 있다
- 4CI 자동 테스트와 QA 수동 검증의 역할 분담을 설명할 수 있다
테스트 피라미드
빠르고 싼 것을 많이, 느리고 비싼 것을 적게
/\ E2E (적게) — 실제 사용자 흐름 전체. 느리고 깨지기 쉬움(flaky)
/ \ 통합 (중간) — 모듈·외부(DB/API) 연동 검증
/____\ 단위 (많이) — 함수/클래스 단위. 빠르고 싸고 안정적
피드백 속도: 단위(ms) > 통합(초) > E2E(분)
유지 비용: 단위(낮음) < 통합 < E2E(높음, UI 바뀌면 깨짐)
핵심: 빠른 피드백(CI/CD 파이프라인의 fail-fast)은 단위 테스트에서 옵니다. E2E를 너무 많이 쌓으면(역피라미드) 파이프라인이 느려지고 flaky(간헐 실패)해져, 팀이 빨간불을 무시하기 시작합니다. E2E는 '가장 중요한 몇 개 흐름'만 핵심으로 둡니다.
확대
위 그림처럼 단위 테스트를 바닥에 가장 많이, E2E를 꼭대기에 적게 두는 피라미드 구조가 빠른 피드백과 안정적인 파이프라인을 만듭니다.
커버리지의 진실 — 실행 ≠ 검증
80%라는 숫자가 속이는 것
커버리지는 '테스트가 실행한 코드 비율'입니다. 함정이 둘입니다.
함정 1: 실행만 하고 단언(assert) 안 함
test("결제", () => { pay(1000); }); // 호출만, 결과 검증 없음
→ 커버리지엔 잡히지만(실행됨) 버그는 못 잡음(틀려도 통과)
함정 2: 숫자 채우기
중요한 결제·인증은 안 짜고, 쉬운 getter/setter로 80% 채움
→ 정작 위험한 곳은 미검증
올바른 관점: 커버리지는 참고 지표일 뿐입니다. 중요한 것은 **"위험한 흐름(결제·인증·데이터 정합)이 의미 있게 검증되는가"**입니다. 커버리지 목표를 KPI로 강제하면 '숫자 채우기'라는 기술 부채와 리팩터링 성격의 부작용이 생깁니다. PM은 "커버리지 몇 %"가 아니라 "핵심 시나리오(요구사항 정의의 인수기준)가 테스트로 있는가"를 물어야 합니다.
확대
위 그림처럼 커버리지 숫자가 높아도 assert 없이 실행만 하면 버그를 못 잡고, 낮아도 핵심 경로를 단언하면 실질 안전성이 높습니다.
확대
위 그림처럼 스모크는 배포 직후 핵심 기능 생존 여부를 빠르게 확인하고, 회귀는 기존 기능이 새 변경으로 깨지지 않음을 포괄적으로 검증합니다.
핵심 흐름 검증 점검 — 직접 확인
PM·QA·인프라는 '커버리지 %'가 아니라 '위험한 흐름이 테스트로 보호되는가'를 점검합니다.
핵심 시나리오 ↔ 테스트 매핑:
결제 성공 → 단위✓ 통합✓ E2E✓
결제 실패 재시도 → 단위✓ 통합✗ ← 빠짐! 위험
중복 결제 방지(멱등) → 단위✗ ← 빠짐! 사고 직결
로그인/권한 → 단위✓ E2E✓
→ 커버리지는 80%여도 '결제 실패·중복'이 미검증이면 실질 리스크 높음
배포 게이트:
□ 스모크 테스트가 핵심 경로(홈/로그인/결제시작)를 커버하나?
□ 과거 장애가 회귀 테스트로 박제됐나?
점검 결과:
커버리지 82% (양호해 보임)
그러나 '중복 결제 방지' 테스트 없음 → 가장 비싼 사고가 미검증
→ 숫자보다 "어떤 흐름이 검증되나"가 중요. 결제 예외/중복부터 보강
echo '인수기준 → 대응 테스트 존재 여부 매핑'- 커버리지 숫자보다 "핵심 시나리오(결제·인증·중복방지)에 대응 테스트가 있나"를 먼저 본다 — 80%여도 위험 흐름이 비면 실질 리스크 큼
- 테스트가 호출만 하고 단언(expect/assert)이 없으면 커버리지엔 잡혀도 버그는 못 잡음 → "검증의 질"을 확인
- E2E 테스트가 자주 깨지면(flaky) 팀이 빨간불을 무시 → 신뢰 붕괴(CI/CD 파이프라인). E2E는 핵심 소수만, 단위로 무게중심 이동
- 과거 장애가 회귀 테스트로 박제됐는지 확인 — 안 됐으면 같은 버그가 재발 가능. 장애 포스트모템(SLO·에러버짓·포스트모템)의 액션으로 테스트 추가
상황: "커버리지 80% 달성"을 목표로 테스트를 늘렸는데, 쉬운 코드 위주로 숫자를 채우고 정작 결제의 중복 방지(멱등) 로직은 테스트가 없었습니다. 결국 동기 vs 비동기의 메시지 중복 상황에서 중복 결제 사고가 터집니다.
원인: 커버리지 숫자를 목표로 삼아 검증의 질이 아니라 양을 좇았습니다. 가장 위험한 흐름(돈이 걸린 결제·중복)이 미검증으로 남았습니다.
진단:
□ 결제 성공/실패/중복/환불 각각에 단언이 있는 테스트가 있나?
□ 과거 발생한 결제 사고가 회귀 테스트로 박제됐나?
□ 커버리지 높은 영역이 '쉬운 코드'에 쏠려 있지 않나?
해결: (1) 커버리지를 KPI에서 '참고 지표'로 강등하고, 위험 기반 테스트(돈·인증·데이터 정합 흐름 우선)로 전환. (2) 모든 장애는 SLO·에러버짓·포스트모템의 포스트모템에서 회귀 테스트로 박제 — 같은 버그 재발을 CI가 막게. (3) 결제 같은 핵심은 인수기준(요구사항 정의)의 정상·예외·경계가 모두 테스트로 존재하는지 확인. 테스트의 가치는 줄 수가 아니라 '무엇을 보호하는가'에 있습니다.
심화 — 초록불이 늘 진실은 아니다
심화: 피라미드를 지켜도 새는 곳 — 목(mock)의 표류와 재시도의 유혹
피라미드대로 단위 테스트를 두껍게 쌓았는데도 운영에서 깨지는 팀이 많습니다. 커버리지 함정 다음으로 알아야 할 것은, 테스트 그 자체가 거짓 신뢰를 만드는 경로입니다.
- 목(mock)은 실제와 함께 늙지 않습니다: 단위 테스트는 외부 의존(결제사 API·다른 팀 서비스)을 목으로 대체합니다. 문제는 실제 API가 바뀌어도(필드 추가·에러 코드 변경) 목은 옛 모습 그대로라는 것 — 테스트는 초록불인데 운영 연동만 깨집니다. 목이 많아질수록 '실제와 목의 표류(drift)'를 잡아줄 통합·계약(contract) 테스트가 함께 필요합니다.
- 자동 재시도는 신호를 죽입니다: 간헐 실패(flaky)에 'retry 2회'를 걸면 파이프라인은 조용해지지만, 실패가 테스트 결함인지 진짜 동시성 버그인지 구분하지 않고 전부 통과시킵니다. 재시도 전 1차 실패율을 별도 지표로 남기지 않으면, 재시도는 문제를 고치는 게 아니라 숨깁니다.
- 테스트 스위트에도 시간 예산이 필요합니다: 테스트가 쌓여 CI가 10분에서 40분이 되면 개발자 행동이 바뀝니다 — 커밋을 몰아서 올리고, 빨간불을 기다리지 않고, 게이트 우회를 요청합니다(CI/CD 파이프라인의 fail-fast가 무너짐). 스위트 실행 시간을 지표로 추적하고 병렬화·선택 실행으로 예산 안에 묶어야 피라미드가 유지됩니다.
- 규모에 따라 피라미드 모양도 다릅니다: 로직이 두꺼운 모놀리스는 단위 중심이 맞지만, 서비스 간 호출이 본질인 MSA에서는 '연동이 깨지는' 사고가 많아 통합·계약 테스트의 비중이 올라갑니다. 피라미드는 교리가 아니라 '실패가 실제로 어디서 나는가'에 맞춘 배분입니다.
그래서 성숙한 팀은 초록불의 '질'을 따로 감사합니다 — 목이 실제와 맞는지, 재시도가 몇 번 일어났는지, 스위트가 몇 분 걸리는지를 '테스트의 테스트'로 봅니다.
상황: 주문 E2E 테스트가 주 2~3회 간헐 실패해 파이프라인 신뢰를 깎아먹자, 팀이 재시도 2회 옵션을 켰습니다. 이후 석 달간 CI는 전부 초록불이었는데, 프로모션 트래픽이 몰린 날 운영에서 같은 쿠폰이 동시 요청에 두 번 적용되는 사고가 났습니다.
원인: 그 간헐 실패는 flaky가 아니라 진짜 동시성 버그의 간헐적 재현이었습니다. 쿠폰 적용 검증이 타이밍에 따라 실패하고 있었는데, 재시도가 실패 원인을 분류하지 않고 전부 통과시켜 신호를 지웠습니다. 초록불의 실체는 '문제 없음'이 아니라 '두 번째 시도에서 우연히 통과'였습니다.
진단: CI 로그에서 재시도로 통과한 실행만 골라 보면, 1차 실패가 무작위 단계가 아니라 항상 같은 단계(쿠폰 중복 적용 검증) 에서 났습니다. 실패가 특정 지점에 몰리면 환경 문제가 아니라 제품 버그를 의심해야 합니다.
해결: (1) 재시도로 통과한 빌드를 '성공'이 아니라 별도 상태로 집계하고, 1차 실패율에 임계를 둬 초과 시 조사를 강제합니다. (2) 간헐 실패 테스트는 즉시 격리(quarantine)하되 반드시 티켓과 기한을 붙입니다 — 격리만 하고 잊으면 커버리지 구멍이 됩니다. (3) 실패 원인을 '테스트 결함/환경/제품 버그'로 분류하는 규칙을 두고, 제품 버그로 판명되면 회귀 테스트로 박제합니다. 재시도는 편의 기능이지, 실패의 면죄부가 아닙니다.
인프라/SRE로서 테스트 전략은 CI/CD 파이프라인 게이트의 품질을 결정합니다 — 단위 테스트로 빠른 피드백을, 스모크 테스트로 배포 직후 핵심 경로 생존을, 회귀 테스트로 과거 장애 재발 방지를 보장합니다. flaky E2E를 격리해 파이프라인 신뢰를 지키는 것도 플랫폼팀의 일입니다. PM은 "커버리지 몇 %"가 아니라 "결제·인증 같은 핵심 시나리오가 테스트로 보호되는가"를 품질 기준으로 삼고, 모든 장애의 포스트모템 액션에 '회귀 테스트 추가'를 포함시켜 같은 사고가 반복되지 않게 합니다. 좋은 테스트는 비용이 아니라, 두려움 없이 빠르게 변경하게 하는 속도의 기반입니다.
다음 모듈에서는 시간이 지나며 쌓이는 '기술 부채'와 리팩터링을, PM이 이해해야 할 비용 관점에서 다룹니다.
실전 랩으로 손에 익히기: 테스트 전략 실습 — 테스트 피라미드 비중과 무엇을 어느 레벨에서 테스트할지 판단합니다.