API 자동화 테스트로 UI 없이 비즈니스 로직 검증하기
API 자동화 테스트의 계약 검증, 테스트 데이터 격리, 서비스 가상화, CI/CD 게이팅 전략과 pytest 구현 예시를 정리한다.
2026-08-14 · 최초 발행 2025-12-18
UI를 통과하지 않고 계약과 로직을 검증하는 방식
API 자동화 테스트는 애플리케이션의 HTTP/gRPC API를 코드로 호출해 입력, 처리, 출력에 걸친 비즈니스 로직과 계약(Contract)을 확인하는 테스트 체계다. UI 변화에 영향을 덜 받으면서 서비스 계약과 데이터 일관성을 검증할 수 있어, 마이크로서비스와 클라우드 네이티브 환경의 회귀 검증에 잘 맞는다.
테스트 범위는 컴포넌트, 통합, 소비자 주도 계약(Consumer-driven), 엔드투엔드 수준까지 계층화할 수 있다. 외부 의존성은 모킹이나 서비스 가상화로 분리하고, 실행 결과가 매번 달라지지 않도록 데이터와 환경을 통제한다. UI 비의존성, 빠른 실행, 낮은 플레이키(flaky)율, CI/CD에서의 병렬 확장성이 이 방식의 핵심 속성이다.
명세와 데이터가 테스트의 기준점이 된다
OpenAPI, AsyncAPI, Pact 같은 명세는 테스트 케이스 생성과 계약 준수 검증의 기준이 된다. 스키마, 필수 필드, 오류 코드가 일관되게 유지되는지 확인하고, 스펙 변경이 하위 호환성을 깨뜨리는지 계약 테스트로 일찍 발견할 수 있다.
테스트 데이터는 시드 데이터, 팩토리, 트랜잭션 롤백을 활용해 결정론적으로 준비한다. 데이터 경합과 실행 순서 의존성을 줄이고, 멱등성 키와 버전 락으로 재시도 또는 중복 호출이 상태를 오염시키지 않게 한다.
외부 시스템은 WireMock이나 Prism 등으로 가상화할 수 있다. 네트워크와 서드파티의 변동성을 격리하되, 실제 호출이 필요한 경로만 선택적으로 통과시키면 현실성과 실행 속도 사이의 균형을 조절할 수 있다.
검증은 상태 코드나 스키마에만 머물지 않는다. 도메인 규칙 어썰션을 로그, 트레이스, 메트릭과 연결하면 실패 원인을 추적하기 쉬워진다. 슬로우 쿼리와 p95 지연 같은 비기능 검증도 기능 검증 결과와 함께 하나의 리포트로 다룰 수 있다.
실행부터 실패 처리까지의 경로
입력에는 명세, 테스트 데이터, 환경 변수와 시크릿, 모킹 설정이 포함된다. 스펙 파싱과 케이스 생성, 트랜잭션 기반 데이터 셋업을 거쳐 병렬 테스트와 일관성 검사를 수행한다.
5xx, 타임아웃, 스키마 불일치가 발생하면 리트라이와 회로 차단을 적용한다. 재시도 한계를 넘긴 실패는 이슈로 등록하고 관련 아티팩트를 보존한다.
UI 테스트와 비교할 때 달라지는 운영 특성
| 지표(운영 관점) | UI 테스트 | API 자동화 테스트 |
|---|---|---|
| 성능(평균 실행 시간) | 초~분 단위 | ms~초 단위 |
| 안정성(플레이키율) | 5~20% 빈번 | 0.5~3% 낮음 |
| 일관성(환경 의존성) | 브라우저/렌더링 영향 큼 | 네트워크·데이터에 국한, 제어 용이 |
| 확장성(병렬성) | 브라우저 리소스 병목 | 경량 프로세스 병렬 확장 용이 |
| 운영 편의(유지보수) | UI 변경에 민감 | 스펙 기반 자동화로 변화 흡수 용이 |
주: 수치는 일반적 경향값이며 시스템·도구·조직 성숙도에 따라 변동 가능.
계약 위반과 외부 연동을 검증하는 장면
결제와 정산 흐름에서는 승인, 매입, 취소의 멱등성, 금액 정합성, 환불 경합 락을 자동 검증할 수 있다. BFF와 앱 백엔드 사이에서는 Pact를 사용해 앱 릴리스 전에 계약 위반을 막고 하위 호환성을 확인한다.
환율이나 신용조회처럼 외부 시스템에 의존하는 경로는 WireMock으로 가상화해 야간 배치 회귀를 안정화할 수 있다. 인증·인가 실패, 속도 제한, 입력 유효성처럼 방어 로직이 필요한 경로도 API 수준에서 네거티브 테스트로 검증한다.
범위 선정부터 관측까지 이어지는 운영 설계
중요 도메인 경로 중 수익과 위험에 연결되는 흐름을 먼저 선정하고, CRUD, 경계값, 오류 케이스를 포함한다. REST Assured(Java), pytest+requests(Python), Postman+Newman(CLI), Pact(계약), WireMock/Prism(모킹), k6/JMeter(성능)처럼 목적에 맞는 도구를 고를 수 있다. 단일 스택 표준화는 운영을 단순하게 하지만, 팀별 자율 도구 선택은 개발 속도 측면의 장점이 있다.
명세 품질은 OpenAPI 엄격 모드(예: spectral lint)와 스키마 버저닝(semver), 소비자 매트릭스를 이용한 변경 영향 분석으로 강화한다. 엄격한 검증은 초기 진입 장벽을 높일 수 있다.
데이터는 시드와 팩토리, 임시 DB(에페멀), 트랜잭션 롤백, 멱등성 키를 조합해 관리한다. 민감정보는 합성하거나 마스킹하고, 시간여행(고정된 시계)으로 실행의 결정론성을 높인다. 현실 데이터가 부족하면 커버리지가 특정 조건에 치우칠 위험이 있다.
외부 의존성은 기본적으로 모킹하고, 핵심 경로에는 실제 호출을 섞는 하이브리드 구성이 가능하다. 느리거나 비결정적인 경로는 계약 기반 스텁으로 다룬다. 이는 현실성을 낮출 수 있지만 속도와 안정성을 얻는 선택이다.
CI/CD에서는 변경 파일을 기준으로 테스트를 선택 실행하고 샤딩과 병렬화를 적용한다. 계약 위반은 하드 스톱으로 두고, 실패율과 p95 지연 임계치를 품질 게이트에 반영할 수 있다. 게이트가 과도하면 배포 민첩성이 떨어질 수 있다.
JUnit XML/Allure, 엔드포인트·스키마 필드 커버리지, Grafana 메트릭을 연결하고 플레이키 탐지와 퀘런틴, 자동 재시도 룰을 관리한다. 시크릿은 Vault로 관리하며 최소 권한을 적용하고, 테스트 아티팩트에 데이터가 노출되지 않게 해야 한다. 감사 추적에는 누가, 무엇을, 언제 수행했는지를 남긴다.
pytest, requests, jsonschema로 계약을 확인하는 예시
전제조건은 Python 3.11+와 로컬 또는 CI 러너다. 설치 명령은 pip install pytest requests jsonschema다.
테스트 스키마 예시(order_created.json)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["orderId", "status", "amount"],
"properties": {
"orderId": {"type": "string"},
"status": {"type": "string", "enum": ["CREATED", "PAID", "CANCELLED"]},
"amount": {"type": "number", "minimum": 0}
},
"additionalProperties": false
}
테스트 코드(test_orders.py)
import os, time, uuid, requests, json
from jsonschema import validate
BASE_URL = os.getenv("BASE_URL", "http://localhost:8080")
def retry_request(method, url, **kwargs):
# 5xx/타임아웃 2회 재시도(지수 백오프)
backoff = 0.2
for attempt in range(3):
try:
resp = requests.request(method, url, timeout=3, **kwargs)
if resp.status_code >= 500:
raise requests.HTTPError(f"server error {resp.status_code}")
return resp
except Exception:
if attempt == 2:
raise
time.sleep(backoff)
backoff *= 2
def load_schema(name):
with open(name, "r", encoding="utf-8") as f:
return json.load(f)
def test_health():
r = retry_request("GET", f"{BASE_URL}/health")
assert r.status_code == 200
assert r.headers.get("content-type", "").startswith("application/json")
def test_create_order_idempotent(tmp_path):
idem_key = str(uuid.uuid4())
payload = {"amount": 19.99}
headers = {"Idempotency-Key": idem_key, "Content-Type": "application/json"}
# 첫 호출
r1 = retry_request("POST", f"{BASE_URL}/v1/orders", headers=headers, json=payload)
assert r1.status_code in (200, 201)
schema = load_schema("order_created.json")
validate(instance=r1.json(), schema=schema)
order_id = r1.json()["orderId"]
# 동일 키로 재호출(멱등성 검증)
r2 = retry_request("POST", f"{BASE_URL}/v1/orders", headers=headers, json=payload)
assert r2.status_code in (200, 201)
assert r2.json()["orderId"] == order_id
환경 변수는 BASE_URL=http://localhost:8080으로 설정하고 pytest -q를 실행한다. 멱등성 키와 스키마 검증은 결정론성과 계약 보장에 사용하며, 5xx와 타임아웃에만 제한적으로 재시도해 과도한 재시도를 피한다. 동일한 스크립트를 로컬과 CI에서 사용할 수 있다.
회귀 속도와 배포 안정성에 미치는 영향
회귀 검증은 UI 기준 3060분에서 API 기준 28분으로 바뀌며, 10~30배 단축과 개발자 피드백 루프 가속을 기대할 수 있다. 플래키율은 <2% 달성을 목표로 하고, 재시도와 퀘런틴을 통해 배포 중단 빈도를 낮춘다.
계약 위반과 스키마 불일치를 일찍 검출하면 릴리스 결함을 20~40% 줄여 장애 유입률을 낮출 수 있다. 동일 커버리지 기준으로 경량 병렬화를 적용하면 CI 러너 비용도 20% 이상 절감할 수 있다.
핵심 경로와 계약 테스트부터 시작한 뒤 데이터 전략, 관측, 게이팅을 점진적으로 넓히면 속도와 안정성을 함께 다룰 수 있다.