TDD와 테스트 피라미드로 설계하는 테스트 전략
테스트 전략의 핵심은 테스트가 있는지보다 피드백이 충분히 빠르고 신뢰 가능한지에 있다. TDD, 테스트 피라미드, 도구 선택, AWS 연동 테스트와 flaky 대응 기준을 연결해 실무 판단 기준으로 정리한다.
Script Companion
오디오와 함께 스크립트 보기
- 01
테스트 전략의 출발점은 코드가 처음 작성되는 시간보다 읽히고 수정되는 시간이 훨씬 길다는 사실입니다. 테스트가 없으면 새 기능이 기존 기능을 깨뜨리는 회귀 버그를 늦게 발견하고, 리팩터링은 어디가 터질지 모른다는 공포가 됩니다. 배포 전 수동 확인 항목이 늘어나 배포 속도도 느려지고, 새로운 팀원은 테스트라는 실행 가능한 예시 없이 코드의 의도를 추측해야 합니다. 그래서 테스트는 단순한 안전장치가 아니라 빠른 피드백, 살아있는 문서, 설계 개선, 배포 자신감을 함께 제공하는 작업 방식입니다.
- 02
테스트 전략이 없던 시기의 기본값은 개발 후 수동 QA이거나 브라우저 조작을 녹화한 E2E 테스트였습니다. 처음에는 쉬워 보이지만, UI 문구나 DOM 구조, 인증 플로우가 조금만 바뀌어도 녹화 스크립트를 다시 만들고 실패 원인을 프론트엔드, 백엔드, 테스트 환경 중 어디에서 찾아야 하는지 추적해야 합니다. Martin Fowler는 이런 UI 중심 자동화가 느리고 깨지기 쉬워 빌드 시간을 늘린다고 설명했고, 그래서 낮은 수준의 테스트를 더 많이 두는 피라미드 구조가 필요하다고 봅니다. 핵심 질문은 테스트가 있느냐가 아니라, 피드백이 고칠 수 있을 만큼 빠르고 믿을 만하냐입니다.
- 03
Google Testing Blog의 사례는 이 문제를 숫자로 보여줍니다. E2E 중심 팀은 릴리스 전 90% 통과 기준을 두었지만, 환경 장애와 파트너 서비스 실패, 큰 버그가 작은 버그를 가리는 현상 때문에 마일스톤을 1주 늦게 끝냈고 개발자는 수정 결과를 다음 날에야 확인했습니다. flaky 테스트 분석에서는 전체 테스트 실행의 약 1.5%가 flaky였고, 테스트의 약 16%가 어떤 수준의 flakiness를 보였으며, pass에서 fail로 바뀌는 전환의 약 84%가 flaky 테스트와 관련됐습니다. 이 숫자는 수동 확인을 더 열심히 하는 방식만으로는 회귀 위험을 줄이기 어렵다는 점을 보여줍니다.
- 04
TDD는 이 문제를 Red, Green, Refactor 사이클로 나눕니다. Red 단계에서는 먼저 실패하는 테스트로 무엇을 만들어야 하는지 고정하고, Green 단계에서는 그 테스트를 통과하는 최소 코드만 작성합니다. Refactor 단계에서는 이미 통과하는 테스트를 보호망으로 삼아 구조를 개선합니다. 이 분리는 인지 부하를 줄입니다. 일반적인 개발에서는 무엇을 만들지, 어떻게 만들지, 잘 동작하는지를 동시에 생각해야 하지만, TDD는 이 세 가지 사고를 단계별로 분리합니다. 그래서 완료 기준이 명확해지고, 수 분 단위의 즉각적 피드백을 받으며, Green 단계에서 YAGNI 원칙이 자연스럽게 지켜집니다.
- 05
하지만 TDD는 모든 상황에서 같은 효과를 내지 않습니다. UI와 UX를 탐색하며 요구사항이 주 1~2회 바뀌는 단계라면 Red-Green 유지 비용이 이득보다 커질 수 있고, 이때는 Spike로 모양을 먼저 잡은 뒤 테스트를 붙이는 편이 낫습니다. 알고리즘이나 인터페이스가 아직 정해지지 않았다면 Prototype 후 TDD가 적절하고, 레거시 코드는 Characterization Test로 현재 동작을 먼저 고정해야 합니다. CI 테스트 실행 시간이 10분을 넘으면 Red-Green 피드백이 너무 느려지므로 Slow Test 격리나 병렬화가 필요합니다. 1주 이내 폐기할 프로토타입이나 PoC라면 테스트 ROI가 없어서 TDD 생략도 허용됩니다.
- 06
커버리지도 숫자만으로 판단하면 위험합니다. 문서의 기준은 80% 이상을 목표로 하되 Business Logic인 Service는 100%를 목표로 하고, app.module.ts나 main.ts 같은 설정 파일은 제외하는 것입니다. 커버리지가 60% 미만이면 회귀 감지 신뢰도가 낮아 TDD 도입 효과가 반감됩니다. 다만 CI 10분 임계도 단독 규칙이 아닙니다. Integration 테스트가 늘어 PR 빌드가 7분에서 14분으로 증가했다면 PR CI에서는 Unit만 5분 안에 돌리고 nightly CI에서 Integration을 묶어 실행할 수 있습니다. 이 선택은 PR-merge 사이클을 줄이는 대신 integration 회귀 발견을 다음 날로 늦추므로, 주 1~2회 prod 배포하는 팀과 시간당 배포하는 팀의 판단이 달라집니다.
- 07
TDD를 적용할지 결정할 때는 요구사항이 불변식으로 표현되는지 봅니다. 정산 금액은 음수가 될 수 없다거나 주문 상태는 paid, shipped, delivered 순서만 허용한다는 식이면 테스트를 먼저 쓰기에 좋습니다. 반대로 UX 탐색처럼 인터페이스가 하루에도 여러 번 바뀌는 작업은 화면과 API 모양을 먼저 고정한 뒤 테스트를 붙이는 편이 낫습니다. TDD가 조용히 실패하는 대표 사례는 Mock이 구현 세부사항만 검증하는 경우입니다. repository.save가 한 번 호출됐는지만 확인하고 반환 DTO나 DB 제약을 검증하지 않으면, 저장 전 금액 반올림 로직을 삭제해도 테스트가 초록색일 수 있습니다. NestJS의 overrideProvider와 overrideGuard도 빠른 피드백을 위한 도구이지 실제 상호작용 검증을 영구히 생략하라는 뜻은 아닙니다.
- 08
테스트 피라미드는 빠르고 저렴한 Unit Test를 바닥에 많이 두고, Integration Test를 중간에 두며, 느리고 비싼 E2E Test를 꼭대기에 적게 두는 구조입니다. 문서의 기본 비율은 Unit 70%, Integration 20%, E2E 10%입니다. Unit Test는 하나의 함수나 클래스 메서드를 보고 DB와 외부 API를 Mock으로 대체하므로 빠르고 격리됩니다. Integration Test는 실제 DB나 Redis와 Service, Repository가 함께 동작하는지 확인해 Mock과 실제 동작의 괴리를 잡습니다. E2E Test는 HTTP 요청부터 응답까지 전체 흐름을 클라이언트 입장에서 검증하지만, 실행이 느리고 설정이 복잡하며 실패 원인 파악이 어렵기 때문에 핵심 API 경로만 커버합니다.
- 09
피라미드가 말하는 진짜 원리는 빠른, 격리된, 다수 신호가 바닥에 있고 느린, 통합된, 소수 신호가 꼭대기에 있어야 한다는 비용과 피드백의 곡선입니다. 이 원리는 testing 밖에도 전이됩니다. Hibri Marzook은 production monitoring을 TDD의 outer loop라고 부르며, unit-level metric alert, service-level SLO alert, end-user funnel alert의 계층을 설명합니다. progressive delivery에서는 Ring 0 내부 dogfood, Ring 1 canary 1~5%, Ring 2 beta 10~25%, Ring 3 GA 100%가 비슷한 신호 구조를 가집니다. 데이터 검증에서도 schema constraint, cross-row invariant, cross-system reconciliation이 각각 빠른 로컬 검증부터 느린 시스템 간 대조까지 이어집니다.
- 10
Google Testing Blog는 2015년에 첫 추정값으로 70/20/10을 제안했지만, 이후 글들은 단일 비율 자체보다 의사결정 좌표를 보라고 확장합니다. 2017년 분석에서는 Google의 small, medium, large 테스트 중 flaky 비율이 각각 0.5%, 1.6%, 14%였고, RAM 사용량과 flakiness의 상관 r2가 0.76, 바이너리 크기와 flakiness의 상관 r2가 0.82로 나타났습니다. 2024년 SMURF 글은 Speed, Maintainability, Utilization, Reliability, Fidelity를 함께 보는 프레임으로 확장했습니다. 따라서 중요한 질문은 E2E를 정확히 10%로 맞췄느냐가 아니라, 큰 테스트가 늘면서 flaky 비율, CI 대기, 원인 특정 시간이 함께 증가하고 있느냐입니다.
- 11
NestJS와 프론트엔드 경험을 연결하면 도구 감각이 더 명확해집니다. 프론트에서 Jest와 React Testing Library로 컴포넌트를 테스트했다면, 백엔드 테스트도 같은 Jest 위에서 움직입니다. 다만 render로 Button을 렌더링하던 대상이 request로 API를 호출하는 HTTP 엔드포인트로 바뀌고, screen으로 화면 텍스트를 찾던 검증이 response.body를 기대값과 비교하는 방식으로 바뀝니다. Jest는 describe, it 또는 test, beforeEach와 beforeAll, jest.fn, mockResolvedValue, toHaveBeenCalledWith, toMatchSnapshot 같은 기본 도구를 제공합니다. Supertest는 실제 HTTP 서버 없이 요청을 시뮬레이션하고, Testcontainers는 Docker 컨테이너로 테스트용 DB나 Redis를 실행한 뒤 종료합니다.
- 12
AWS SQS와 SNS 연동 코드는 테스트 경계 선택이 더 중요합니다. 실제 AWS 계정에 의존하면 테스트 비용이 생기고, 네트워크 의존으로 flaky 테스트가 늘며, CI에서 IAM 권한 관리가 복잡해집니다. 그래서 SQS 호출 파라미터를 검증할 때는 aws-sdk-client-mock이 빠른 단위 테스트로 적합합니다. 반대로 메시지 발행에서 소비까지의 흐름, SNS에서 SQS로 이어지는 구독, DLQ redrive policy, FIFO deduplication처럼 AWS 서비스 간 계약이 맞아야 드러나는 문제는 LocalStack과 Testcontainers를 쓰는 통합 테스트가 필요합니다. 판단 기준은 AWS를 Mock할 것인가가 아니라, 검증하려는 실패가 어느 경계에서 발생하느냐입니다.
- 13
실무 트러블슈팅도 같은 원리로 정리할 수 있습니다. 테스트 간 DB 상태 오염은 테스트 A에서 INSERT한 데이터가 테스트 B에 남아 있거나 Auto Increment ID가 예상과 달라지는 문제이며, beforeEach에서 DB 초기화가 빠졌는지 봐야 합니다. Jest 메모리 누수는 NestJS 테스트 모듈이 afterAll에서 닫히지 않거나 대용량 Mock 데이터, 미처리 Promise, 닫히지 않은 DB 커넥션 때문에 생길 수 있습니다. 비동기 테스트 타임아웃은 await 누락, Testcontainers 시작 지연, 실제 외부 API 호출 때문에 발생할 수 있습니다. TypeORM Repository Mock은 메서드가 많아 일부만 정의하면 TypeScript 타입 오류가 나기 쉽고, E2E의 JWT Guard는 인증 없는 요청을 거부하므로 Guard Mock과 실제 JWT 발급 중 무엇을 검증할지 나눠야 합니다.
- 14
Flaky 테스트는 단순히 다시 돌리면 되는 불편함이 아니라 의사결정 신호를 오염시키는 문제입니다. 같은 코드에서 pass와 fail이 모두 나오면 다시 돌리면 되겠지라는 습관이 생기고, 실제 회귀도 false positive처럼 취급됩니다. 문서는 1000개 테스트가 있는 프로젝트에서 flaky 실행률 1.5%면 약 15개 테스트가 실패해 조사 비용이 생길 수 있다고 설명합니다. 그래서 발견 기준은 재실행해서 통과했는지가 아니라, 이 테스트를 critical path에 둘 만큼 신뢰할 수 있느냐입니다. 동일 커밋에서 3회 중 1회라도 실패하면 quarantine 후보로 분리하고, production race condition을 가리는지 확인한 뒤 시간 의존성, 실행 순서 의존성, 외부 리소스 의존성을 고정해야 합니다.
- 15
정리하면 테스트 전략은 TDD로 요구사항을 테스트로 정의하고, 테스트 피라미드로 Unit, Integration, E2E의 역할을 나누는 일입니다. Unit은 빠른 피드백과 격리를 담당하고, Integration은 Mock과 실제 인프라 사이의 차이를 잡으며, E2E는 사용자가 만나는 핵심 흐름을 좁게 확인합니다. Mock과 테스트 더블은 속도를 얻기 위한 수단이고, 실제 경계 검증을 생략하는 면허가 아닙니다. 커버리지, CI 시간, flaky 비율, 배포 빈도는 서로 분리된 숫자가 아니라 한 팀의 테스트 전략을 조정하는 좌표입니다. 이 좌표를 함께 보면 코드 변경 시 회귀 버그 위험을 자동 검증 구조로 옮길 수 있습니다.
같은 레이어
L9에서 이어 듣기
- 설계 원칙을 운영 가능한 코드로 잇기 길이 미정
- Clean Architecture의 의존성 규칙 길이 미정
- DDD 기본기: 도메인 언어와 경계 설계 길이 미정
- Twelve-Factor App 운영 원칙 길이 미정
- CAP과 일관성으로 보는 분산 시스템 선택 길이 미정
- MSA 패턴, 분리의 이득과 운영 비용 길이 미정
- Saga Pattern: 로컬 커밋과 역순 보상 길이 미정
- CQRS와 이벤트 소싱의 운영 경계 길이 미정
- 대규모 웹 크롤러의 큐, 정중함, 중복 제거 길이 미정
- API 계약으로 안전하게 서비스 경계를 진화시키기 길이 미정
- URL Shortener와 Rate Limiter로 보는 시스템 디자인 길이 미정