JSON Schema 검증 실전: API 계약 테스트로 안정적인 마이크로서비스 구축하기
현대적인 소프트웨어 아키텍처, 특히 마이크로서비스 아키텍처(MSA) 환경에서 서비스 간의 통신은 매우 빈번하게 일어납니다. 이때 각 서비스는 서로 약속된 데이터 형식을 주고받는데, 이를 'API 계약(API Contract)'이라고 부릅니다. 만약 API 제공자(Producer)가 예고 없이 응답 데이터의 필드 타입을 변경하거나 필수 필드를 삭제한다면 어떻게 될까요? 연결된 수많은 소비자(Consumer) 서비스들은 즉시 런타기 에러를 발생시키며 도미노처럼 무너질 것입니다.
이러한 'Breaking Changes'를 방지하고, 시스템의 안정성을 보장하기 위한 가장 강력하고 실무적인 방법이 바로 JSON Schema를 활용한 계약 테스트(Contract Testing)입니다. 본 가이드에서는 JSON Schema 검증의 실전 기법과 이를 통해 어떻게 견고한 API 생태계를 구축할 수 있는지 깊이 있게 다룹니다.
1. API 계약 테스트의 본질과 JSON Schema의 역할
API 계약(Contract)의 정의와 중요성
API 계약이란 클라이언트와 서버 간의 상호작용에 대한 규약입니다. "어떤 엔드포인트로 요청을 보내야 하는가?", "요청 바디에 어떤 필드가 포함되어야 하는가?", "응답으로 어떤 데이터 타입이 돌아오는가?"에 대한 명세가 바로 계약입니다. 이 계약이 깨지는 순간, 분산 시스템의 신뢰도는 급격히 하락합니다.
API 변경이 시스템에 미치는 영향 (Breaking Changes)
전통적인 통합 테스트(Integration Test)는 모든 서비스를 띄워놓고 테스트해야 하므로 비용이 매우 높습니다. 반면, 계약 테스트는 서비스 전체를 구동하지 않고도 데이터 구조의 정합성을 확인할 수 있습니다. API의 필드명이 user_id에서 userId로 변경되는 단순한 작업조차도, 적절한 검증 로직이 없다면 전체 시스템의 장애로 이어질 수 있습니다.
JSON Schema를 활용한 자동화된 검증의 이점
JSON Schema는 JSON 데이터의 구조를 정의하기 위한 표준화된 포맷입니다. 이를 활용하면 다음과 같은 이점을 얻을 수 있습니다. * 언어 독립성: Java, Python, Node.js, Go 등 어떤 언어로 작성된 서비스라도 동일한 스키마로 검증 가능합니다. * 선언적 검증: "무엇을 검증할 것인가"를 코드가 아닌 데이터 구조로 선언하여 가독성을 높입니다. * 자동화 용이성: CI/CD 파이프라인에 통합하여 배포 전 단계에서 데이터 규격 오류를 즉시 차단할 수 있습니다. 만약 복잡한 스키마를 설계하는 데 어려움을 겪고 있다면, JSON Schema 생성 및 검증 도구를 활용하여 구조를 빠르게 설계해 보는 것을 추천합니다.
Rivet 2. JSON Schema의 핵심 구성 요소와 검증 규칙
효과적인 계약 테스트를 위해서는 단순한 타입 체크를 넘어, 데이터의 비즈니스 로직을 반영할 수 있는 정교한 제약 조건을 설정해야 합니다.
기본 데이터 타입 및 필수 필드(required) 설정
가장 기초적인 단계는 type을 정의하고, 반드시 존재해야 하는 필드를 required 배열에 명시하는 것입니다.
* string, number, integer, boolean, object, array, null 등의 타입을 명확히 구분해야 합니다.
* required 항목에 누락된 필드가 있으면 검증은 즉시 실패합니다.
제약 조건(Constraints)을 통한 데이터 무결성 확보
단순히 타입만 맞다고 해서 안전한 것은 아닙니다. 데이터의 범위와 형식을 제한해야 합니다.
* Numeric Constraints: minimum, maximum, multipleOf 등을 사용하여 숫자 범위를 제한합니다.
* String Constraints: minLength, maxLength, 그리고 pattern(정규표현식)을 사용하여 이메일 형식, 전화번호 형식 등을 검증합니다.
* Array Constraints: minItems, maxItems, uniqueItems를 통해 배열의 크기와 중복 여부를 제어합니다.
* Enum: enum 키워드를 사용하여 허용된 특정 값(예: ['PENDING', 'SUCCESS', 'FAILED'])만 들어오도록 제한합니다.
복잡한 구조를 위한 객체 및 배열 검증
API 응답은 대개 중첩된(Nested) 객체나 배열을 포함합니다. properties 내부에 다시 type: "object"를 정의하여 계층 구조를 깊게 내려가며 검증할 수 있습니다. 이때 additionalProperties: false 설정을 사용하면, 정의되지 않은 알 수 없는 필드가 응답에 포함되는 것을 방지하여 더욱 엄격한 계약을 유지할 수 있습니다.
3. 실전! JSON Schema를 활용한 API 검증 구현하기
이제 실제 개발 환경에서 어떻게 이 검증 로직을 코드로 구현하고 테스트할 수 있는지 살펴보겠습니다. 가장 널리 사용되는 JavaScript 라이브러리인 Ajv(Another JSON Schema Validator)를 예로 들어 설명하겠습니다.
테스트 시나리오 설계: 유효한 데이터 vs 유효하지 않은 데이터
좋은 계약 테스트는 '성공 케이스'뿐만 아니라 '실패 케이스'를 반드시 포함해야 합니다.
1. Positive Test: 스키마에 완벽히 부합하는 페이로드가 들어왔을 때 true를 반환하는지 확인.
2. Negative Test: 필수 필드 누락, 잘못된 데이터 타입, 범위를 벗어난 숫자, 잘못된 정규식 패턴 등이 포함되었을 때 정확한 에러 메시지를 생성하는지 확인.
[Code Block] Node.js와 Ajv를 이용한 실전 검증 예제
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true }); // 모든 에러를 한 번에 보고하도록 설정
// 1. API 계약(Schema) 정의
const userApiSchema = {
type: "object",
properties: {
id: { type: "integer" },
username: { type: "string", minLength: 3 },
email: { type: "string", pattern: "^\\S+@\\S+\\.\\S+$" },
role: { enum: ["admin", "user", "guest"] },
tags: {
type: "array",
items: { type: "string" },
uniqueItems: true
}
},
required: ["id", "username", "email"],
additionalProperties: false // 정의되지 않은 필드 허용 안 함
};
// 2. 검증할 데이터 (테스트 케이스)
const validData = {
id: 101,
username: "dev_master",
email: "tester@supertools.tw",
role: "admin",
tags: ["nodejs", "api", "testing"]
};
const invalidData = {
id: "not-a-number", // Error: type mismatch
username: "de", // Error: minLength violation
email: "invalid-email", // Error: pattern violation
extraField: "unexpected" // Error: additionalProperties violation
};
// 3. 검증 로직 실행
const validate = ajv.compile(userApiSchema);
console.log("--- Valid Data Test ---");
const isDataValid = validate(validData);
console.log(isDataValid ? "✅ Success: Data matches contract." : "❌ Fail: " + ajv.errorsText(validate.errors));
console.log("\n--- Invalid Data Test ---");
const isInvalidDataValid = validate(intvalidData);
if (!isInvalidDataValid) {
console.log("❌ Detected expected errors:");
validate.errors.forEach(err => {
console.log(` - Path: ${err.instancePath}, Message: ${err.message}`);
});
}
CI/CD 파이프라인에 검증 단계 통합하기
이 검증 로직은 개발자의 로컬 환경에서만 돌아가서는 안 됩니다. GitHub Actions, Jenkins, GitLab CI와 같은 파이프라인의 Test 단계에 포함시켜야 합니다. API 서버의 코드가 변경되어 배포되기 직전, 스키마 검증 테스트가 실패한다면 배포 프로세스를 즉시 중단(Fail-fast)시켜 장애 확산을 막아야 합니다.
4. 고급 검증 전략: 조건부 로직과 스키마 재사용
단순한 구조를 넘어, 비즈니스 로직에 따라 데이터 구조가 동적으로 변해야 하는 경우가 있습니다.
oneOf, anyOf, allOf를 활용한 다형성 처리
API 응답이 상황에 따라 다른 형태를 가질 때 유용합니다.
* oneOf: 응답이 반드시 정의된 여러 스키마 중 단 하나와 일치해야 할 때 (예: 결제 성공 응답 vs 결제 실패 응답).
* anyOf: 응답이 정의된 스키마 중 하나 이상과 일치해야 할 때.
* allOf: 응답이 정의된 모든 스키마 조건을 동시에 만족해야 할 때 (스키마 확장 시 사용).
if-then-else를 이용한 동적 스키마 검증
특정 필드의 값에 따라 다른 필드의 유효성을 검사해야 할 때 사용합니다. 예를 들어, type 필드가 "credit_card"라면 card_number 필드가 필수적으로 존재해야 하고, "paypal"이라면 email 필드가 있어야 한다는 로직을 스키마 자체에 내장할 수 있습니다.
스키마 모듈화와 $ref를 통한 유지보수성 향상
모든 스키마를 하나의 파일에 넣는 것은 불가능합니다. 공통으로 사용되는 Address, User, ErrorResponse 등의 구조를 별도 파일로 분리하고, $ref 키워드를 사용하여 참조하십시오. 이는 스키마의 재사용성을 높이고, 한 곳의 수정이 모든 관련 스키마에 반영되도록 하여 유지보수 비용을 획기적으로 줄여줍니다.
5. 테스트 전략 비교: 무엇을 선택할 것인가?
API 테스트에는 여러 계층이 존재합니다. 각 테스트의 목적과 비용을 이해하고 적절한 균형을 맞추는 것이 중요합니다.
| 테스트 유형 | 목적 | 장점 | 단점 |
|---|---|---|---|
| 단위 테스트 (Unit Test) | 개별 함수/로직의 정확성 검증 | 매우 빠름, 버그 조기 발견 | 전체 시스템 흐름 파악 불가 |
| 계약 테스트 (Contract Test) | 서비스 간 데이터 규격(Schema) 일치 확인 | 빠르고 가벼움, 서비스 간 의존성 분리 | 실제 네트워크/DB 환경 미반영 |
| 통합 테스트 (Integration Test) | 여러 모듈/서비스의 상호작용 검증 | 실제 비즈니스 흐름 확인 가능 | 실행 속도가 느리고 환경 설정 복잡 |
| E2E 테스트 (End-to-End) | 사용자 시나리오 기반 전체 시스템 검증 | 실제 사용자 경험과 가장 유사 | 매우 느리고 유지보수 비용이 매우 높음 |
6. API 계약 관리의 베스트 프랙티스
하위 호환성(Backward Compatibility) 유지 전략
계약 테스트의 궁극적인 목적은 하위 호환성을 지키는 것입니다.
* 확장 지향적 설계: 새로운 필드를 추가하는 것은 안전하지만, 기존 필드를 삭제하거나 이름을 바꾸는 것은 금지해야 합니다.
* Deprecation 정책: 기존 필드를 제거해야 한다면, 일정 기간 deprecated 상태로 유지하고 로그를 통해 클라이언트에게 알리는 프로세스를 갖추어야 합니다.
스키마 버전 관리와 문서화 자동화
스카마는 코드와 함께 버전 관리(Git)되어야 합니다. 또한, 작성된 JSON Schema를 기반으로 OpenAPI (Swagger) 문서를 자동 생성하도록 설정하십시오. 문서와 실제 구현(Schema)이 일치하지 않는 '문서 드리프트(Documentation Drift)' 현상을 방지하는 유일한 방법입니다.
더 다양한 개발 생산성 향상 도구가 필요하다면 Super Tools의 다양한 개발 도구를 탐색해 보세요.
FAQ (자주 묻는 질문)
Q1. JSON Schema 검증이 성능에 큰 영향을 미치나요? A1. 아주 거대한 JSON 페이로드(수십 MB 이상)를 검증할 때는 CPU 부하가 발생할 수 있습니다. 하지만 일반적인 API 응답 크기에서는 Ajv와 같은 최적화된 라이브러리를 사용할 경우 성능 저하는 무시할 수 있는 수준입니다.
Q2. 스키마 버전 관리는 어떻게 하는 것이 좋나요?
A2. API 엔드포인트에 버전을 포함(예: /v1/users)하거나, HTTP 헤더를 통해 버전을 명시하는 방식을 권장합니다. 스키마 파일 또한 schemas/v1/user.json과 같이 디렉토리 구조로 관리하는 것이 명확합니다.
Q3. 외부 API(Third-party)의 응답도 검증할 수 있나요? A3. 네, 가능합니다. 외부 API의 응답 샘플을 바탕으로 스키마를 작성하여 우리 시스템으로 들어오는 데이터의 무결성을 검증하는 것은 매우 훌륭한 방어적 프로그래밍 기법입니다.
Q4. Swagger(OpenAPI)와 JSON Schema의 차이점은 무엇인가요? A4. OpenAPI는 API의 전체적인 구조(엔드포인트, 메서드, 인증, 응답 등)를 설명하는 프레임워크이며, JSON Schema는 그 안에서 데이터의 구체적인 형태를 정의하는 하위 표준입니다. 즉, OpenAPI는 JSON Schema를 포함합니다.
Q5. 정규표현식(pattern)을 너무 복잡하게 쓰면 위험한가요? A5. 네, 매우 복잡한 정규표현식은 'ReDoS(Regular Expression Denial of Service)' 공격의 대상이 될 수 있습니다. 가능한 단순하고 명확한 패턴을 사용하고, 복잡한 로직은 애플리케이션 계층에서 별도로 검증하는 것이 안전합니다.
Q6. 스키마 검증 실패 시 클라이언트에게 에러 메시지를 그대로 보여줘야 하나요?
A6. 아니요. 스키마의 상세한 에러 메시지(예: pattern mismatch)는 내부 구조를 노출할 수 있으므로, 로그에는 상세히 기록하되 클라이언트에게는 "Invalid Request Format"과 같은 추상화된 메시지를 전달하는 것이 보안상 좋습니다.
결론
JSON Schema를 이용한 계약 테스트는 단순한 데이터 검증을 넘어, 분산 시스템의 '신뢰의 기반'을 만드는 작업입니다. 이는 개발자 간의 의사소통 비용을 줄여주고, 배포에 대한 두려움을 제거하며, 결과적으로 시스템의 가용성을 극대화합니다.
처음에는 스키마를 설계하고 관리하는 것이 번거롭게 느껴질 수 있습니다. 하지만 서비스가 커지고 복잡해질수록, 잘 구축된 JSON Schema는 자동화된 방어막이 되어 여러분의 시스템을 예상치 못한 장애로부터 지켜줄 것입니다. 지금 바로 여러분의 API 프로젝트에 엄격한 계약 테스트를 도입해 보세요.