티스토리 뷰

테스트를 많이 쓰는 팀이 항상 잘 운영되는 것은 아닙니다. 모든 함수에 단위 테스트를 붙이고, 모든 화면에 E2E 테스트를 추가하면 겉보기에는 촘촘해 보일 수 있습니다. 그런데 CI가 30분씩 걸리고, 플래키 테스트가 늘고, PR 속도가 급격히 떨어지면 그 체계는 버그보다 느린 개발 속도를 더 많이 만들 수 있습니다.

그래서 마지막에는 수량보다 배치를 봐야 합니다. 어떤 계층에 얼마를 투자할지, 어디를 두껍게 보호할지, 무엇을 문서가 아니라 팀 습관으로 남길지를 결정하는 일이 전략입니다.

이 글은 Testing 101 시리즈의 마지막 글입니다. 여기서는 테스트 피라미드의 분포, 계층별 투자 대비 효과, 계약 테스트와 팀 운영 습관, 그리고 전략을 살아 있는 규칙으로 유지하는 방법을 정리하겠습니다.

Testing 101 10장 흐름 개요
Testing 101 10장 흐름 개요

테스트 전략은 팀의 가치관과 위험도 평가로 결정됩니다. 정답은 없지만 의식적인 선택이 필요합니다.

먼저 던지는 질문

  • 테스트 피라미드는 왜 분포가 중요할까요?
  • 단위, 통합, E2E 계층에 어떤 비율로 투자해야 할까요?
  • 중요한 사용자 경로는 어떻게 정할까요?

왜 중요한가

테스트는 공짜가 아닙니다. 작성 시간, 실행 시간, 유지 시간, 실패를 고치는 시간이 모두 듭니다. 전략 없이 늘리기만 하면 느리고 깨지기 쉬운 테스트 묶음이 됩니다.

반대로 분포를 잘 잡으면 적은 수의 E2E로 큰 사용자 사고를 막고, 많은 수의 단위 테스트로 빠른 피드백을 유지할 수 있습니다. 전략은 품질과 속도 사이의 균형을 잡는 일입니다.

한눈에 보는 구조

테스트 피라미드는 단순한 그림이 아니라 비용 구조를 보여 줍니다. 아래층일수록 빠르고 많아야 하고, 위층일수록 비싸고 적어야 합니다. 이 분포가 무너지면 피드백 속도와 신뢰가 함께 흔들립니다.

핵심 용어

  • 테스트 피라미드: 단위 테스트는 많고, 통합 테스트는 그보다 적고, E2E 테스트는 더 적게 두는 분포 모델입니다.
  • ROI: 투자한 비용 대비 얼마나 많은 버그를 잡는지 보는 관점입니다.
  • 핵심 경로(critical path): 로그인, 결제처럼 사용자 피해가 큰 흐름입니다.
  • 계약 테스트(contract test): 시스템 경계에서 입력과 출력 형식을 검증하는 테스트입니다.
  • 플래키 예산(flaky budget): 허용 가능한 불안정 비율을 수치로 정한 기준입니다.

테스트 철학 비교 — 피라미드, 트로피, 다이아몬드

테스트 분포에 대한 접근 방식은 하나만 있는 것이 아닙니다. 다음 표는 세 가지 주요 철학을 비교한 것입니다.

모델 비율 (단위:통합:E2E) 주장자 적합한 프로젝트
테스트 피라미드 70:20:10 Mike Cohn, Martin Fowler 백엔드 API, 도메인 로직 중심 시스템
테스트 트로피 50:30:20 (static 추가) Kent C. Dodds 프론트엔드 중심, 사용자 인터랙션 중요
테스트 다이아몬드 30:50:20 일부 팀 마이크로서비스, 경계 테스트 중심

테스트 피라미드는 단위 테스트를 가장 많이 두고 E2E를 최소화합니다. 테스트 트로피는 통합 테스트 비중을 높이고 정적 분석을 포함합니다. 테스트 다이아몬드는 시스템 경계에서 계약 테스트를 두껍게 유지하는 접근입니다.

팀은 자신의 아키텍처와 위험도에 따라 적합한 모델을 선택해야 합니다.

바꾸기 전과 후

바꾸기 전 — 전략 없이 계층을 늘린 상태

- 모든 함수에 단위 테스트
- 모든 시나리오에 E2E 테스트
- CI 30분, PR 처리 속도 정체

바꾼 뒤 — 투자 위치를 조정한 상태

- 핵심 도메인 단위 테스트 2,000개
- 통합 테스트 200개 (DB와 외부 API 경계)
- E2E 20개 (결제, 로그인 같은 핵심 경로)
- CI 5분 이내

차이는 테스트 수보다 분포에 있습니다. 모든 계층을 같은 강도로 밀어붙이는 대신, 빠른 계층은 두껍게, 비싼 계층은 핵심만 남기는 방식입니다.

다섯 단계로 전략 만들기

1단계 — 현재 분포를 먼저 측정하기

pytest --collect-only -q | wc -l    # 전체 테스트 수
ls tests/unit | wc -l
ls tests/integration | wc -l
ls tests/e2e | wc -l

2단계 — 핵심 경로 정의하기

- Login
- Payment
- Sign-up
- Password reset

이 경로는 E2E로 반드시 보호해야 할 후보입니다.

3단계 — 경계에 계약 테스트 추가하기

# tests/contracts/test_payment_api.py
def test_payment_response_schema():
    res = payment_client.charge(amount=100)
    assert set(res.keys()) >= {"id", "status", "amount"}

4단계 — 팀 운영 습관 만들기

- PR 템플릿: "회귀 테스트를 추가했는가? [ ]"
- 매주 30분: 플래키 테스트 검토
- 매달: 커버리지 절대값이 아니라 추세 확인

5단계 — 분기마다 가지치기하기

# 6개월 동안 한 번도 가치 있게 실패하지 않은 E2E는 재검토 대상
# 같은 영역에서 반복 실패하는 테스트는 리팩터링 신호

이 코드에서 먼저 볼 점

  • 전략은 문서 한 장보다 반복되는 운영 습관으로 유지됩니다.
  • 측정하지 않으면 현재 분포를 알 수 없습니다.
  • 핵심 경로를 정의하지 않으면 모든 경로가 중요해 보이기 시작합니다.

그래서 전략 문서만 만들어 두는 것은 충분하지 않습니다. PR 템플릿, 주간 점검, 야간 잡, 커버리지 추세 확인처럼 반복되는 루틴이 함께 있어야 전략이 살아 있습니다.

어디서 자주 헷갈릴까요?

가장 흔한 실수는 모든 코드에 같은 강도로 테스트를 요구하는 일입니다. 위험이 높은 결제 로직과 단순한 화면 래퍼 코드는 같은 투자 기준으로 다루기 어렵습니다.

둘째, E2E를 주력 계층으로 삼는 문제입니다. 현실과 가깝다는 이유로 과하게 늘리면 속도와 안정성을 동시에 잃습니다.

셋째, 커버리지 숫자만 보고 전략이 잘 굴러간다고 판단하는 경우입니다. 어떤 코드를 덮고 있는지가 더 중요합니다. 시스템 경계에서 계약 테스트가 빠지면 외부 API 변경은 운영에서 처음 드러날 수 있습니다.

예산 배분 예시와 이유

실제 팀에서는 테스트 분포를 예산 개념으로 접근할 수 있습니다. 다음은 중간 규모 백엔드 팀의 예산 배분 예시입니다.

테스트 예산 — 2000개 테스트 기준

계층 개수 비율 평균 실행 시간 총 시간 이유
단위 테스트 1400 70% 10ms 14초 도메인 로직, 변환 함수, 유효성 검사
통합 테스트 400 20% 100ms 40초 DB, 외부 API, 메시지 큐 경계
E2E 테스트 200 10% 2초 400초 로그인, 결제, 주문 핵심 경로
합계 2000 100% 7분 30초

배분 이유

  • 단위 테스트는 빠르고 결정적이므로 가장 많이 둡니다. 도메인 로직의 모든 분기를 촘촘히 덮습니다.
  • 통합 테스트는 시스템 경계에서 입력과 출력 형식을 검증합니다. 외부 의존이 바뀌어도 빠르게 감지합니다.
  • E2E 테스트는 사용자 영향이 큰 핵심 경로만 남깁니다. 모든 시나리오를 E2E로 덮으면 유지비가 급격히 늘어납니다.

이 예산은 팀마다 다를 수 있지만, 분포의 의도를 명시하는 것이 중요합니다.

직접 검증해 볼 것

  1. 현재 저장소의 단위/통합/E2E 테스트 수와 각 잡의 실행 시간을 표로 적어 봅니다. 전략은 감이 아니라 현재 분포를 알아야 시작할 수 있습니다.
  2. 로그인, 결제, 비밀번호 재설정처럼 사용자 피해가 큰 핵심 경로를 세 개만 골라 현재 어떤 계층이 보호하는지 표시합니다.
  3. 지난 분기 플래키 테스트 목록과 CI 평균 시간을 같이 놓고, 어떤 계층을 줄이거나 옮겨야 하는지 회고합니다.

예상 결과: 모든 테스트를 늘리는 계획이 아니라, 위험이 큰 경로를 어떤 계층에서 어떻게 보호할지 우선순위가 드러나야 합니다.

심화 실습: 운영 관점 테스트 점검

실무에서 테스트를 확장할 때 가장 먼저 해야 할 일은 실패 원인을 사람이 추측하지 않도록 로그와 단언문을 정리하는 것입니다. 테스트 실패 메시지에는 입력값, 기대값, 실제값이 함께 남아야 하며, 그래야 CI 로그만으로도 원인을 좁힐 수 있습니다.

또한 테스트는 코드와 함께 진화해야 합니다. 기능이 바뀌었는데 테스트가 그대로라면 테스트는 안전장치가 아니라 오경보 장치가 됩니다. 그래서 팀에서는 요구사항 변경 PR에 테스트 변경이 함께 포함되는지를 리뷰 기준으로 두는 편이 좋습니다.

fixture는 단순 편의 기능이 아니라 설계 도구입니다. 어떤 객체를 기본 상태로 두는지, 어떤 상태 변형을 허용하는지 fixture 레이어에서 명확히 정의하면 테스트 의도가 깔끔해집니다. 특히 도메인 객체가 복잡할수록 fixture 설계 품질이 테스트 유지보수 비용을 좌우합니다.

회귀 버그를 줄이려면 버그 티켓이 닫힐 때 반드시 재현 테스트를 남겨야 합니다. 수정 코드만 머지하면 같은 원인의 버그가 다른 경로에서 재발합니다. 반대로 재현 테스트를 함께 남기면 팀 지식이 실행 가능한 형태로 축적됩니다.

커버리지 리포트는 주간 회고에서 매우 유용합니다. 숫자만 보는 대신 누락 라인이 핵심 도메인인지 확인하고, 다음 스프린트에서 보강할 테스트를 합의하면 테스트 투자가 산발적으로 흩어지지 않습니다.

CI에서는 실패를 빠르게 보여 주는 순서가 중요합니다. 일반적으로 단위 테스트를 먼저 실행하고, 그 다음 통합 테스트, 마지막으로 느린 E2E를 배치하면 평균 피드백 시간이 줄어듭니다. 파이프라인 설계도 테스트 전략의 일부로 다루어야 합니다.

실무에서 테스트를 확장할 때 가장 먼저 해야 할 일은 실패 원인을 사람이 추측하지 않도록 로그와 단언문을 정리하는 것입니다. 테스트 실패 메시지에는 입력값, 기대값, 실제값이 함께 남아야 하며, 그래야 CI 로그만으로도 원인을 좁힐 수 있습니다.

또한 테스트는 코드와 함께 진화해야 합니다. 기능이 바뀌었는데 테스트가 그대로라면 테스트는 안전장치가 아니라 오경보 장치가 됩니다. 그래서 팀에서는 요구사항 변경 PR에 테스트 변경이 함께 포함되는지를 리뷰 기준으로 두는 편이 좋습니다.

fixture는 단순 편의 기능이 아니라 설계 도구입니다. 어떤 객체를 기본 상태로 두는지, 어떤 상태 변형을 허용하는지 fixture 레이어에서 명확히 정의하면 테스트 의도가 깔끔해집니다. 특히 도메인 객체가 복잡할수록 fixture 설계 품질이 테스트 유지보수 비용을 좌우합니다.

회귀 버그를 줄이려면 버그 티켓이 닫힐 때 반드시 재현 테스트를 남겨야 합니다. 수정 코드만 머지하면 같은 원인의 버그가 다른 경로에서 재발합니다. 반대로 재현 테스트를 함께 남기면 팀 지식이 실행 가능한 형태로 축적됩니다.

커버리지 리포트는 주간 회고에서 매우 유용합니다. 숫자만 보는 대신 누락 라인이 핵심 도메인인지 확인하고, 다음 스프린트에서 보강할 테스트를 합의하면 테스트 투자가 산발적으로 흩어지지 않습니다.

CI에서는 실패를 빠르게 보여 주는 순서가 중요합니다. 일반적으로 단위 테스트를 먼저 실행하고, 그 다음 통합 테스트, 마지막으로 느린 E2E를 배치하면 평균 피드백 시간이 줄어듭니다. 파이프라인 설계도 테스트 전략의 일부로 다루어야 합니다.

실무에서 테스트를 확장할 때 가장 먼저 해야 할 일은 실패 원인을 사람이 추측하지 않도록 로그와 단언문을 정리하는 것입니다. 테스트 실패 메시지에는 입력값, 기대값, 실제값이 함께 남아야 하며, 그래야 CI 로그만으로도 원인을 좁힐 수 있습니다.

또한 테스트는 코드와 함께 진화해야 합니다. 기능이 바뀌었는데 테스트가 그대로라면 테스트는 안전장치가 아니라 오경보 장치가 됩니다. 그래서 팀에서는 요구사항 변경 PR에 테스트 변경이 함께 포함되는지를 리뷰 기준으로 두는 편이 좋습니다.

fixture는 단순 편의 기능이 아니라 설계 도구입니다. 어떤 객체를 기본 상태로 두는지, 어떤 상태 변형을 허용하는지 fixture 레이어에서 명확히 정의하면 테스트 의도가 깔끔해집니다. 특히 도메인 객체가 복잡할수록 fixture 설계 품질이 테스트 유지보수 비용을 좌우합니다.

from unittest.mock import patch

def test_payment_service_retries_once_on_timeout():
    service = PaymentService()
    with patch('src.payment.client.charge') as charge:
        charge.side_effect = [TimeoutError(), {'status': 'ok'}]
        result = service.pay(user_id='u-1', amount=10000)

    assert result['status'] == 'ok'
    assert charge.call_count == 2
pytest -q --maxfail=1 --disable-warnings
pytest --cov=src --cov-report=term-missing

실패 신호와 첫 점검

  • 모든 코드에 같은 강도로 테스트를 요구하면 비용 대비 효과가 낮은 계층부터 팀이 우회하기 시작합니다.
  • E2E 수가 계속 늘어나는데도 핵심 사고가 줄지 않으면 계층 배치가 잘못된 경우가 많습니다.
  • 플래키 비율과 CI 시간을 재지 않으면 전략이 실제로 좋아지는지 판단할 근거가 없습니다.

프로젝트 유형별 전략

테스트 전략은 프로젝트 성격에 따라 달라집니다. 다음은 세 가지 대표 유형과 권장 접근 방식입니다.

API 서버

API 서버는 도메인 로직과 HTTP 경계가 분명합니다. 단위 테스트로 비즈니스 규칙을 두껍게 보호하고, 통합 테스트로 요청/응답 스키마를 검증합니다. E2E는 인증 흐름과 주요 엔드포인트 체인 정도만 남깁니다.

CLI 도구

CLI 도구는 입력 파싱과 출력 형식이 중요합니다. 단위 테스트로 개별 명령 로직을 검증하고, 통합 테스트로 실제 파일 입출력과 플래그 조합을 확인합니다. E2E는 전체 명령 체인을 실행해 종단 동작을 검증합니다.

데이터 파이프라인

데이터 파이프라인은 변환 로직과 스키마 일관성이 핵심입니다. 단위 테스트로 개별 변환 함수를 검증하고, 통합 테스트로 실제 데이터베이스나 객체 스토리지와의 경계를 확인합니다. E2E는 전체 파이프라인 실행과 데이터 품질 검증에 집중합니다.

실무에서는 이렇게 생각합니다

성숙한 팀은 목표 분포와 플래키 예산을 아예 운영 기준으로 문서화합니다. 새 서비스가 생겨도 같은 기준으로 출발하고, 분기마다 CI 시간과 불안정 비율을 점검합니다.

경험 많은 엔지니어는 테스트 전략을 기술 선택이 아니라 의사결정 체계로 봅니다. 무엇을 E2E에 남길지, 무엇을 단위 테스트로 내릴지, 어떤 회귀는 반드시 PR에 포함할지 모두 팀 속도와 위험도를 함께 보고 정합니다.

테스트 부채 관리

테스트는 코드와 마찬가지로 부채가 쌓입니다. 다음은 테스트 부채를 관리하는 전략입니다.

테스트 부채의 신호

  • 플래키 테스트가 계속 늘어납니다.
  • 테스트가 깨져도 팀이 수정 대신 비활성화를 선택합니다.
  • 새 기능 추가 시 기존 테스트 수정 시간이 기능 개발 시간보다 깁니다.
  • CI 실행 시간이 15분을 넘어 PR 피드백이 느려집니다.

부채 관리 전략

1. 분기마다 플래키 테스트 목록을 리뷰하고 수정 또는 삭제합니다.
2. 6개월 동안 한 번도 실패하지 않은 E2E 테스트는 가치를 재평가합니다.
3. 같은 영역에서 반복 실패하는 테스트는 리팩터링 신호로 봅니다.
4. 테스트 실행 시간을 메트릭으로 추적하고 임계값을 설정합니다.

예시 — 플래키 테스트 추적

# tests/conftest.py
import pytest

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()
    if report.when == "call" and report.outcome == "failed":
        # 실패 로그를 수집하거나 외부 시스템에 기록
        print(f"FLAKY: {item.nodeid}")

이 훅을 사용하면 실패한 테스트를 자동으로 추적하고, 반복 실패 패턴을 분석할 수 있습니다.

테스트 부채는 방치하면 팀의 배포 속도와 신뢰를 동시에 깎습니다. 정기적인 정리와 메트릭 기반 관리가 필수입니다.

체크리스트

  • 팀의 테스트 분포를 알고 있습니다.
  • 핵심 경로가 문서화되어 있습니다.
  • PR 템플릿에 회귀 테스트 항목이 있습니다.
  • 플래키 비율이나 CI 시간을 꾸준히 측정합니다.

테스트 전략 문서화

테스트 전략은 팀의 머리속에만 있으면 인수인계가 어렵습니다. 다음은 효과적인 문서화 구조입니다.

# Testing Strategy

## 1. 목표 분포
- Unit: 70%
- Integration: 20%
- E2E: 10%

## 2. 핵심 경로(E2E 필수)
- Login flow
- Payment flow
- Password reset

## 3. 품질 지표
- CI time: < 5 min (PR)
- Flaky rate: < 3%
- Coverage: 80% (unit + integration)

## 4. 팀 습관
- PR template: "회귀 테스트를 추가했는가? [ ]"
- Weekly: 플래키 테스트 리뷰 30분
- Quarterly: 전략 회고 1시간

이 문서는 새 멤버가 팀에 합류할 때도, 프로젝트가 커질 때도 일관된 기준을 제공합니다.

연습 문제

  1. 프로젝트의 테스트 분포를 측정하고 피라미드 형태인지 확인해 보세요.
  2. 핵심 경로 세 개를 정하고 E2E가 실제로 덮는지 점검해 보세요.
  3. 팀에 도입할 주간 운영 습관 하나를 제안해 보세요.

정리

테스트 전략은 기법 목록이 아니라 투자 판단입니다. 빠른 테스트를 두껍게 쌓고, 비싼 테스트는 핵심에만 두고, 그 기준을 팀 습관으로 굳혀야 합니다. 이렇게 하면 Testing 101에서 본 단위 테스트, 통합 테스트, E2E 테스트, 회귀 테스트, CI가 하나의 운영 모델로 연결됩니다.

처음 질문으로 돌아가기

  • 테스트 피라미드는 왜 분포가 중요할까요?
    • 본문에서 본 것처럼 테스트는 빠를수록 자주 돌릴 수 있고, 자주 돌수록 회귀를 빨리 잡습니다. 단위 테스트가 가장 빠르고 E2E가 가장 느리기 때문에 빠른 테스트를 많이, 느린 테스트를 적게 두는 분포여야 매 PR마다 충분한 신뢰도를 합리적 시간 안에 얻을 수 있습니다. 분포가 뒤집힌 "아이스크림 콘"은 CI 시간만 늘리고 디버깅을 어렵게 만듭니다.
  • 단위, 통합, E2E 계층에 어떤 비율로 투자해야 할까요?
    • 본문 가이드처럼 가장 빠른 단위 테스트를 가장 많이, 통합은 중간 정도, 느린 E2E는 가장 적게 유지하는 것이 출발점입니다. 다만 절대 비율은 한 가지가 아니라 팀의 위험도에 따라 조정해야 하고, 결제·인증 같은 고위험 경로는 E2E를 더 두텁게, 순수 알고리즘 모듈은 단위 테스트 중심으로 무게중심을 옮기는 식으로 운용합니다.
  • 중요한 사용자 경로는 어떻게 정할까요?
    • 본문에서 강조했듯이 "장애 시 매출·신뢰·법적 리스크가 가장 큰 흐름"을 먼저 식별합니다. 결제 완료, 로그인/회원가입, 데이터 무결성에 관련된 경로가 대표적입니다. 처음부터 완벽한 전략을 세우려 하지 말고, 우선순위가 높은 경로 몇 개에 E2E를 두고 운영 메트릭(에러율·고객 문의 빈도)을 보며 커버리지를 늘려 가는 편이 현실적입니다.

시리즈 목차

참고 자료

실무 참고

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함