테스트 피라미드로 설계하는 Unit·API·UI 품질 게이트
테스트 피라미드의 Unit·API·UI 계층을 기준으로 품질 게이트, 테스트 데이터, CI 파이프라인을 설계하는 방법
2026-08-14 · 최초 발행 2025-12-23
빠른 피드백은 테스트를 아래층에 쌓는 데서 시작한다
테스트 피라미드는 적은 비용으로 빠른 피드백을 얻으면서 품질을 확보하기 위한 계층화 전략이다. Unit, API(통합·계약), UI(E2E) 순으로 테스트 수를 배치하고, 각 계층의 실행 속도와 결과 신뢰성에 맞춰 파이프라인을 구성한다.
일반적인 권장 구성은 Unit 7080%, API 1525%, UI 5~10%다. 다만 제품 특성, 도메인, 리스크 프로필에 따라 이 비율은 조정할 수 있다. UI 테스트가 과도하게 많은 아이스크림 콘 구조는 피드백을 늦추고 불안정성을 키우며 유지보수 비용을 급격히 높인다.
계층마다 검증 대상과 책임을 분리한다
Unit 테스트는 순수 로직을 확인하는 층이다. I/O, 네트워크, 시간 의존성을 차단하고 Mock과 Stub을 적극적으로 사용한다.
API 테스트는 서비스 계약, 스키마, 비즈니스 흐름을 검증한다. 데이터베이스와 메시징을 포함한 통합 범위도 이 계층에서 다룬다.
UI 테스트는 핵심 사용자 여정을 스모크 경로로 제한한다. 크로스 브라우저, 접근성, 시각 회귀 검증도 최소 세트로 유지한다.
재현 가능한 실행 환경도 계층 설계의 일부다. 고정 시드, 고립된 테스트 데이터, 불변(Immutable) 테스트 픽스처를 사용하고, Testcontainers·서비스 가상화·Contract Testing(OpenAPI/Pact)으로 외부 의존성을 통제한다. 테스트 간 독립성, 아이소레이션, 멱등성을 확보할 수 있도록 정리 작업도 포함해야 한다.
품질 게이트는 측정 가능한 조건으로 둔다. Unit 기준 라인≥80%, 분기≥75%, API 스키마 검증률≥95%를 권장하며, 플래키율<1% 유지와 UI 재시도 제한(예: 2회), 격리(Quarantine) 트랙 운영을 함께 고려한다. 시간 기준은 커밋→피드백<10분, 풀리퀘스트 전체 파이프라인<30분을 목표로 삼는다.
실행 특성에 맞춰 테스트를 배치한다
| 구분 | 성능(속도) | 확장성(병렬화) | 일관성(결과 안정성) | 안정성(플레이키 민감도) | 운영 편의 |
|---|---|---|---|---|---|
| Unit | 매우 빠름(ms~초) | 매우 높음(수백 코어) | 매우 높음(결정적) | 매우 높음(안정) | 유지보수 용이, 디버그 용이 |
| API | 중간(초~분) | 높음(서비스 수평 확장) | 높음(계약 기반) | 중간(환경/데이터 영향) | 스키마/계약 관리 필요 |
| UI | 느림(분) | 제한적(브라우저 자원) | 낮음(렌더·타이밍 영향) | 낮음(플레이키 민감) | 인프라/테스트 설계 비용 큼 |
기존 테스트를 옮길 때는 UI에서 검증하던 시나리오 중 API나 Unit으로 내릴 수 있는 대상을 먼저 찾는다. 같은 시나리오가 상·하위 계층에 중복되어 있다면 중복을 최소화한다. 변경 코드에 따라 선택적으로 실행하는 테스트 영향 분석(TIA)도 실행 시간을 줄이는 방법이다.
입력→처리→출력 규격을 테스트 데이터 계약, 스키마, 고정 Seed로 표준화하고, Testcontainers·임시 DB/큐·Fake 서비스로 격리한다. 실패 시 로그와 아티팩트를 즉시 수집하며, 재시도 정책과 격리 큐를 운영한다.
CI에서는 빠른 실패 경로를 먼저 둔다
목표 비율·시간·신뢰성 SLO를 먼저 정한다. 예를 들어 Unit 75%+와 전체 파이프라인 <30분을 기준으로 잡고, 커버리지·플래키·실패 허용도를 게이트 조건으로 명문화할 수 있다. UI 스모크만 필수로 둘지 여부도 이 단계에서 결정한다.
파이프라인은 Unit → API → UI 순으로 배치해 실패를 빠르게 드러낸다. 워크로드는 균등하게 셰딩하고, 캐시와 아티팩트를 재사용한다. 커버리지, 계약 검증, 스모크 통과 조건을 만족했을 때 배포를 승인한다.
플래키 테스트는 타임아웃, 비결정적 대기, 외부 의존성에서 원인을 찾아 제거한다. UI 비중을 줄이고 계약 테스트를 늘리며, 시각 회귀 검증은 핵심 화면에만 남긴다. 결함을 발견하면 테스트를 추가하고 계층 배치를 다시 점검하는 순환을 유지한다.
Unit 테스트: Node.js와 Jest
환경과 버전은 Node.js 20.x, Jest 29.x다. ESM을 사용하지 않는 CJS 구성이며 로컬에서 실행할 수 있다.
package.json
{
"name": "unit-example",
"version": "1.0.0",
"scripts": { "test": "jest --runInBand" },
"devDependencies": { "jest": "^29.7.0" }
}
jest.config.cjs
module.exports = {
testEnvironment: "node",
collectCoverage: true,
coverageThreshold: { global: { lines: 80, branches: 75 } }
};
src/sum.js
function sum(a, b) { return a + b; }
module.exports = { sum };
test/sum.test.js
const { sum } = require('../src/sum');
test('adds 1 + 2 = 3', () => {
expect(sum(1, 2)).toBe(3);
});
API 계약과 스키마를 검증하는 Python 테스트
환경과 버전은 Python 3.11, pytest 8.x, requests 2.x, jsonschema 4.x다. API 엔드포인트는 BASE_URL 환경변수로 지정한다.
requirements.txt
pytest==8.3.2
requests==2.32.3
jsonschema==4.23.0
tests/test_users.py
import os, requests
from jsonschema import validate
BASE_URL = os.getenv("BASE_URL", "http://localhost:8080")
user_schema = {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"}
},
"required": ["id", "name"],
"additionalProperties": False
}
def test_get_user_by_id():
r = requests.get(f"{BASE_URL}/users/1", timeout=3)
assert r.status_code == 200
data = r.json()
validate(instance=data, schema=user_schema)
핵심 사용자 경로만 남기는 Playwright 스모크
환경과 버전은 Node.js 20.x, Playwright 1.50.x다. 핵심 경로 1~3개를 대상으로 하고, 네트워크 안정화를 위해 route와 timeout을 설정한다.
playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
retries: 2,
workers: 4,
timeout: 30_000,
use: { headless: true, baseURL: process.env.BASE_URL || 'http://localhost:3000' },
projects: [{ name: 'chromium', use: { browserName: 'chromium' } }]
});
tests/smoke.spec.ts
import { test, expect } from '@playwright/test';
test('홈→로그인→대시보드 스모크', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: '로그인' }).click();
await page.getByLabel('이메일').fill('test@example.com');
await page.getByLabel('비밀번호').fill('pass1234');
await page.getByRole('button', { name: '로그인' }).click();
await expect(page.getByRole('heading', { name: '대시보드' })).toBeVisible();
});
GitHub Actions로 계층을 연결하기
아래 파이프라인은 Unit → API → UI 순서를 따르고, 실패 시 중단하며 캐시·병렬화·아티팩트 수집을 전제로 한다. Node.js·Playwright와 Python 런타임이 필요하다.
.github/workflows/ci.yml
name: ci
on:
pull_request:
push:
branches: [ main ]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npm test
api:
runs-on: ubuntu-latest
needs: [unit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.11', cache: 'pip' }
- run: pip install -r requirements.txt
- run: pytest -q
env:
BASE_URL: http://localhost:8080
ui:
runs-on: ubuntu-latest
needs: [api]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test -c playwright.config.ts --reporter=line
env:
BASE_URL: http://localhost:3000
서비스 구조에 따른 적용 방식
마이크로서비스에서는 OpenAPI 기반 계약 테스트로 API 안정성을 강화하고, UI E2E는 릴리스 스모크로만 유지한다.
레거시 현대화에서는 Golden Master(Unit/API)를 축적한 뒤 UI 비중을 단계적으로 낮추고 데이터 경계를 개선한다.
데이터베이스 의존성이 큰 경우에는 Testcontainers로 통합 테스트를 안정화하고, 시드 데이터와 마이그레이션을 자동화한다.
테스트 피라미드를 적용하면 빌드·테스트 시간을 4060% 단축하고 커밋→실패 감지 MTTA < 5분을 달성할 수 있다. UI 테스트 수는 70% 이상 축소하면서 플래키율 < 1%를 유지하는 것이 목표다. Shift-left를 통해 장애 조기 검출률을 높이고 결함 수정 비용을 3050% 절감한다.