API 에러 코드: Stripe에서 배운 테스트 스위트 패턴
요약
Stripe의 API 설계 방식을 본떠 에러 코드를 일급 시민으로 취급하고, 이를 기반으로 체계적인 부정적 테스트 스위트를 구축하는 방법을 제안합니다. 에러 응답을 단순한 예외가 아닌 API 계약의 핵심 요소로 정의하여 개발, 프론트엔드, QA의 효율성을 높이는 전략을 다룹니다.
핵심 포인트
- 에러 코드를 문서화된 API 계약의 일부로 취급해야 함
- 에러 코드 카탈로그를 구축하여 부정적 테스트 스위트의 기반으로 활용
- 비즈니스 특정 에러를 명확히 정의하여 프론트엔드 및 QA 대응력 강화
- 성공 경로뿐만 아니라 다양한 실패 시나리오를 테스트 범위에 포함
Stripe의 API 레퍼런스를 한 시간 동안 읽어보면, 모든 엔드포인트가 예시 페이로드(payload)와 함께 열거된 완전한 에러 코드 목록을 가지고 있다는 것을 알게 될 것입니다. 이제 여러분 자신의 API를 살펴보세요.
그 차이는 무시하기 어려울 정도입니다.
Stripe의 API 문서는 에러를 일급 시민(first-class citizens)으로 취급합니다. 모든 엔드포인트는 정상적인 경로(happy path)뿐만 아니라, 구조화된 에러 코드, 설명, HTTP 상태 코드(HTTP status codes), 그리고 예시 응답을 포함하여 예상되는 모든 실패 사례를 명확하게 문서화합니다.
이제 이를 현재 운영 중인 많은 API와 비교해 보십시오.
문서 어딘가에서 일반적인 HTTP 상태 코드 목록을 찾을 수는 있겠지만, 비즈니스 특정 에러(business-specific errors)는 종종 컨트롤러 로직(controller logic) 내부에 묻혀 있거나, 위키 페이지에 흩어져 있거나, 혹은 단순히 문서화되어 있지 않은 경우가 많습니다. 테스트 스위트(test suite)도 별반 다르지 않습니다. 수십 개의 정상 경로 테스트는 존재하지만, 부정적 시나리오(negative scenarios)는 손에 꼽을 정도입니다.
이러한 불균형은 관련된 모든 이들에게 문제를 일으킵니다:
- 개발자들은 어떤 에러가 예상되는지 알지 못합니다.
- 프론트엔드(Frontend) 팀은 실패 상황을 안정적으로 처리할 수 없습니다.
- QA 엔지니어들은 중요한 부정적 케이스(negative cases)를 놓칩니다.
- 리팩터링(Refactoring) 시 아무도 모르게 에러 응답이 실수로 변경됩니다.
몇 년 전, 저는 Stripe의 문서에서 간단한 아이디어를 빌려와 이를 테스트 전략으로 발전시켰습니다.
에러 응답을 예외(exceptions)로 취급하는 대신, 저희는 **에러 코드 카탈로그(error-code catalog)**를 만들고 이를 부정적 테스트 스위트(negative test suite)의 기반으로 삼았습니다.
그 결과는 단순히 더 나은 **API 에러 코드 테스트(API error code testing)**에 그치지 않았습니다. 이는 문서화를 개선하고, 유지보수를 단순화하며, API 계약(API contracts)을 훨씬 더 일관되게 만들었습니다.
이 패턴이 어떻게 작동하는지 소개합니다.
에러 응답이 API 계약의 일부인 이유
사람들이 API 테스트를 생각할 때, 자연스럽게 성공적인 응답에 집중합니다.
전형적인 단언(assertions)에는 다음이 포함됩니다:
- HTTP 200 OK
- HTTP 201 Created
- 올바른 JSON 페이로드 (JSON payload)
- 필수 필드 (Required fields)
- 비즈니스 계산 (Business calculations)
부정적 테스트(Negative testing)는 종종 훨씬 적은 관심을 받습니다.
아마도 다음과 같은 몇 가지 테스트만 있을 것입니다:
- 잘못된 인증 (Invalid authentication)
- 필수 필드 누락 (Missing required fields)
- 알 수 없는 리소스 (Unknown resources)
그 외에도 많은 API가 수동 테스트에 의존하거나 프레임워크가 모든 것을 올바르게 처리하기를 기대하곤 합니다.
문제는 실제 사용자들이 성공적인 요청만큼이나 자주 실패를 경험한다는 점입니다.
예시는 다음과 같습니다:
- 고객 계정 잠김 (Customer account locked)
- 결제 거절 (Payment declined)
- 쿠폰 만료 (Coupon expired)
- 재고 없음 (Inventory unavailable)
- 중복 등록 (Duplicate registration)
- 구독 취소 (Subscription canceled)
- 속도 제한 초과 (Rate limit exceeded)
이것들은 예외적인 시나리오가 아닙니다.
예상되는 비즈니스 결과입니다.
이러한 상황들을 일급 API 계약 (first-class API contracts)으로 취급하면 문서화와 테스트를 설계하는 방식이 바뀝니다.
테스트 입력으로서의 에러 코드 카탈로그 (The Error-Code Catalog as a Test Input)
첫 번째 단계는 API가 의도적으로 반환할 수 있는 모든 비즈니스 에러의 중앙 집중식 카탈로그를 만드는 것입니다.
간소화된 예시는 다음과 같을 수 있습니다:
errors:
USER_NOT_FOUND:
httpStatus: 404
...
이 카탈로그는 단순한 문서 그 이상의 의미를 갖게 됩니다.
이는 실행 가능한 명세 (executable specification)가 됩니다.
"이 엔드포인트는 어떤 에러를 반환해야 하는가?"라고 묻는 대신,
답은 이미 하나의 권위 있는 위치에 존재하게 됩니다.
모든 새로운 비즈니스 에러는 프로덕션 (production)에 반영되기 전에 반드시 여기에 추가되어야 합니다.
이 단 하나의 요구사항이 일관성을 극적으로 향상시킵니다.
카탈로그가 도움이 되는 이유
카탈로그가 없다면:
- 문서가 실제와 어긋납니다 (Documentation drifts).
- 테스트가 불완전해집니다.
- 프론트엔드 팀이 우연히 에러를 발견하게 됩니다.
- 리뷰어가 파괴적 변경 사항 (breaking changes)을 간과합니다.
카탈로그가 있다면:
- 모든 에러가 문서화됩니다.
- 모든 에러가 테스트 가능해집니다.
- 모든 API 소비자 (consumer)가 동일한 계약을 확인합니다.
카탈로그는 자동화를 위한 기반이 됩니다.
카탈로그로부터 생성되는 에러 코드당 하나의 테스트
카탈로그가 존재하면, 네거티브 테스트 (negative tests)를 생성하는 과정은 놀라울 정도로 간단해집니다.
수십 개의 반복적인 테스트를 수동으로 작성하는 대신, 생성기 (generator)가 정의된 모든 에러를 단순히 순회하면 됩니다.
개념적으로는 다음과 같습니다:
for (const errorCode of catalog) {
generateNegativeTest(errorCode);
}
생성된 각 테스트는 다음 네 가지를 검증합니다:
- 예상되는 HTTP 상태 코드 (HTTP status)
- 에러 코드 (error code)
- 에러 메시지 (error message)
- 응답 스키마 (response schema)
EMAIL_ALREADY_EXISTS 사례를 고려해 봅시다.
생성된 시나리오는 다음과 같이 동작할 수 있습니다:
- 사용자를 생성합니다.
- 동일한 사용자를 다시 생성하려고 시도합니다.
- 응답을 검증합니다:
{
"code": "EMAIL_ALREADY_EXISTS",
"message": "Email already exists"
...
구현 방식은 프레임워크에 따라 다르지만, 테스트 철학은 동일합니다:
문서화된 모든 에러는 반드시 그에 상응하는 하나의 테스트를 가져야 합니다.
새로운 에러 코드가 도입되면, 새로운 테스트가 자동으로 나타납니다.
엔지니어가 이를 작성해야 한다는 사실을 기억할 필요가 없습니다.
이것이 더 잘 확장되는 이유
여러분의 API가 다음과 같은 요소를 노출한다고 가정해 봅시다:
- 150개의 엔드포인트 (endpoints)
- 90개의 비즈니스 에러 코드 (business error codes)
이를 수동으로 유지 관리하는 것은 금방 지루한 일이 됩니다.
생성(Generation) 방식은 두 가지 유지 관리 문제를 동시에 해결합니다:
- 누락된 테스트
- 중복된 노력
개발자에게 모든 부정 케이스(negative case)를 기억하라고 요구하는 대신, 카탈로그가 기본 커버리지(baseline coverage)를 보장합니다.
이를 통해 엔지니어는 반복적인 검증 테스트 대신 더 복잡한 비즈니스 워크플로에 집중할 수 있습니다.
조용한 에러 드리프트를 방지하는 형태 검증 (Shape Assertion)
우리가 아주 초기에 배운 교훈 중 하나는 이것입니다:
HTTP 상태 코드만 확인하는 것은 거의 무용지물이라는 점입니다.
어떤 엔드포인트가 원래 다음과 같이 반환한다고 가정해 봅시다:
{
"code": "USER_NOT_FOUND",
"message": "User not found",
...
몇 달 후, 누군가가 전역 예외 처리기 (global exception handler)를 리팩터링합니다.
응답은 다음과 같이 변합니다:
{
"error": "User not found"
}
HTTP 상태 코드는 여전히 다음과 같습니다:
404
많은 테스트가 여전히 통과합니다.
하지만 원래의 응답 규약 (response contract)을 기대하던 모든 클라이언트는 이제 깨지게 됩니다.
이것을 **조용한 에러 드리프트 (silent error drift)**라고 합니다.
소비자(consumers)들이 실패하기 시작할 때까지는 아무런 문제가 없는 것처럼 보입니다.
해결책: 형태 검증 (Shape Assertions)
모든 부정 테스트는 응답 구조 (response structure) 또한 검증합니다.
예시:
expect(response.body).toEqual({
code: expect.any(String),
message: expect.any(String),
...
우리가 단순히 값(values)만 검증하는 것이 아니라는 점에 주목하십시오.
우리는 스키마 (schema) 자체를 검증하고 있는 것입니다.
그 단 하나의 단언 (assertion)이 모든 API 소비자 (consumer)를 예기치 않은 응답 변경으로부터 보호합니다.
이것이 중요한 이유
소비자들은 종종 다음 항목들에 의존합니다:
- 에러 코드 (Error codes)
- 로컬라이제이션 키 (Localization keys)
- 상관관계 ID (Correlation IDs)
- 문서 URL (Documentation URLs)
HTTP 상태 코드가 올바르게 유지되더라도, 이 필드 중 하나라도 제거되면 API의 파괴적 변경 (breaking change)이 될 수 있습니다.
스키마 검증 (Schema validation)은 이러한 문제들을 즉시 포착합니다.
코드를 카탈로그와 동기화 유지하기 (코드 생성, Code Generation)
명백한 우려는 유지보수입니다.
만약 엔지니어들이 다음 두 가지를 모두 수동으로 업데이트해야 한다면:
- 소스 코드 (Source code)
- 에러 카탈로그 (Error catalog)
카탈로그는 결국 시대에 뒤떨어지게 됩니다.
해결책은 코드 생성 (code generation)입니다.
대부분의 애플리케이션은 이미 에러를 중앙에서 정의합니다.
예를 들어:
export enum ErrorCode {
USER_NOT_FOUND,
INVALID_TOKEN,
...
간단한 생성 단계를 통해 다음을 만들어낼 수 있습니다:
- API 문서 (API documentation)
- OpenAPI 컴포넌트 (OpenAPI components)
- 마크다운 참조 테이블 (Markdown reference tables)
- 테스트 입력값 (Test inputs)
- SDK 상수 (SDK constants)
이 모든 것이 동일한 소스로부터 만들어집니다.
이제 에러 정의가 존재하는 곳은 단 한 곳뿐입니다.
나머지 모든 것은 자동으로 생성됩니다.
코드 생성 (Codegen)의 이점
이 접근 방식은 여러 가지 장점을 제공합니다:
문서가 결코 뒤처지지 않음
코드에 새로운 에러가 나타나는 즉시, 문서가 자동으로 업데이트됩니다.
생성된 테스트가 최신 상태를 유지함
수동 동기화가 필요하지 않습니다.
API 소비자의 정렬 유지
클라이언트 SDK는 서버에서 사용하는 것과 동일한 상수를 참조할 수 있습니다.
코드 리뷰가 쉬워짐
새로운 비즈니스 에러를 추가하는 것이 매우 눈에 띄게 됩니다. 왜냐하면 그것이 생성된 문서와 테스트에 영향을 미치기 때문입니다.
우리가 의도적으로 테스트하지 않는 두 가지 에러 코드 (그리고 그 이유)
우리의 네거티브 테스트 스위트 (negative suite)가 거의 모든 비즈니스 에러를 다루고 있음에도 불구하고, 의도적으로 제외하는 두 가지 카테고리가 있습니다.
1. 일반적인 내부 서버 에러 (Generic Internal Server Errors)
예시:
500 Internal Server Error
이것들은 예기치 않은 실패를 나타냅니다.
이것들은 정상적인 비즈니스 동작의 일부가 아닙니다.
모든 가능한 내부 예외 (internal exception)를 의도적으로 발생시키는 대신, 우리는 다음 사항들을 검증합니다:
- 민감한 세부 정보가 노출되지 않는지
- 일반적인 메시지 (generic messages)가 반환되는지
- 상관관계 ID (Correlation IDs)가 존재하는지
- 로깅 (logging)이 올바르게 수행되는지
모든 가능한 서버 실패를 테스트하는 것은 가치가 거의 없습니다.
응답 규약 (response contract)을 테스트하는 것이 훨씬 더 큰 이득을 제공합니다.
2. 인프라 실패 (Infrastructure Failures)
예시로는 다음과 같은 것들이 있습니다:
- 데이터베이스 (Database) 사용 불가
- 네트워크 파티션 (Network partition)
- DNS 장애
- 메시지 브로커 (Message broker) 실패
- 클라우드 스토리지 (Cloud storage) 사용 불가
이러한 실패들은 표준 API 자동화보다는 회복 탄력성 테스트 (resilience testing)에 속합니다.
이들은 다음과 같은 방법을 통해 검증하는 것이 더 좋습니다:
- 카오스 엔지니어링 (Chaos engineering)
- 결함 주입 (Fault injection)
- 인프라 테스트 (Infrastructure testing)
- 재해 복구 (Disaster recovery) 연습
인프라 시나리오를 일상적인 **API 네거티브 테스트 (API negative tests)**에 섞는 것은 대개 불안정한 파이프라인을 생성합니다.
이를 분리하여 유지하는 것이 더 깔끔하고 신뢰할 수 있는 자동화를 결과로 가져옵니다.
우리가 예상하지 못했던 추가적인 이점들
카탈로그가 우리 개발 프로세스의 일부가 되자, 몇 가지 예상치 못한 개선 사항들이 나타났습니다.
더 일관된 API
모든 엔드포인트 (endpoint)가 동일한 응답 형식을 사용하게 되었습니다.
더 나은 프론트엔드 개발
프론트엔드 팀은 더 이상 어떤 에러가 발생할지 추측할 필요가 없게 되었습니다.
더 단순해진 문서화
에러 참조 (Error references)가 자동으로 동기화된 상태를 유지했습니다.
더 깔끔한 풀 리퀘스트 (Pull Requests)
새로운 에러를 추가하는 것이 구현 세부 사항이 아닌 명시적인 설계 결정이 되었습니다.
더 나은 QA 커버리지
네거티브 시나리오 (Negative scenarios)가 성공 시나리오만큼이나 가시화되었습니다.
마치며
대부분의 엔지니어링 팀은 성공적인 요청을 테스트하는 데 많은 투자를 하는 반면, 실패는 부차적인 문제로 취급합니다.
Stripe는 다른 철학을 보여줍니다.
에러는 문서화되고, 표준화되며, 공개 API 규약 (public API contract)의 필수적인 부분으로 취급됩니다.
에러 코드 카탈로그를 구축함으로써 우리는 동일한 사고방식을 채택할 수 있었습니다.
수십 개의 반복적인 에러 응답 테스트 (error response testing) 시나리오를 수동으로 유지 관리하는 대신, 우리는 단일 진실 공급원 (single source of truth)으로부터 이를 생성했습니다.
응답 스키마 검증 (response schema validation) 및 코드 생성 (code generation)과 결합하여, 이 접근 방식은 유지 관리 비용을 극적으로 줄이는 동시에 문서화된 모든 실패 사례가 예상한 대로 정확하게 동작한다는 확신을 높여주었습니다.
만약 여러분의 API에 이미 늘어나는 비즈니스 에러 (business errors) 컬렉션이 있다면, 목록이 관리 불가능해지기 전에 중앙 집중식 카탈로그 (centralized catalog)를 만드는 것을 고려해 보십시오.
투입되는 비용은 상대적으로 적지만, 문서화 품질, 테스트 커버리지 (test coverage), 그리고 장기적인 유지 관리성 (maintainability) 측면에서 얻는 보상은 상당합니다.
자동화된 API 테스트가 어떻게 이 접근 방식을 지원할 수 있는지 알아보고 싶다면, **이 카탈로그 패턴을 체험하기 위한 무료 트라이얼을 시작**하여 생성된 네거티브 테스트 (negative tests), 스키마 검증 (schema validation), 그리고 API 계약 (API contracts)이 실제 환경에서 어떻게 함께 작동하는지 확인해 보시기 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기