신뢰성 높은 API를 위한 API 계약 테스트 구현 방법: 핵심 실전 가이드

Ashley Goolam

Ashley Goolam

18 November 2025

신뢰성 높은 API를 위한 API 계약 테스트 구현 방법: 핵심 실전 가이드

백엔드 API가 예기치 않게 변경되어 프런트엔드 애플리케이션이 갑자기 작동을 멈추는 상황을 경험해 본 적이 있으신가요? 이러한 중단은 전체 시스템에 파급되어 사용자에게 좌절감을 안겨주고 정신없는 디버깅 세션으로 이어질 수 있습니다. 바로 이 지점에서 API 계약 테스트가 중요해집니다. 이는 API 생산자와 소비자 간의 조화를 보장하는 체계적인 접근 방식입니다. 이 가이드에서는 API 계약 테스트의 미묘한 차이를 탐구하고 그 기반, 과제 및 자동화 전략을 살펴보겠습니다. 이 가이드를 마칠 때쯤이면 API 계약 테스트가 단순히 있으면 좋은 것이 아니라 견고한 소프트웨어 개발의 초석인 이유를 이해하게 될 것입니다. API 계약 테스트를 워크플로에 원활하게 통합하는 방법을 함께 탐구하는 이 여정을 시작해 봅시다.

💡
아름다운 API 문서를 생성하는 훌륭한 API 테스트 도구를 원하십니까?

개발팀이 최대한의 생산성을 가지고 함께 작업할 수 있는 통합된 올인원 플랫폼을 원하십니까?

Apidog는 모든 요구사항을 충족하며, 훨씬 더 저렴한 가격으로 Postman을 대체합니다!
버튼

API 계약 테스트란 무엇인가요?

API 계약 테스트는 API 제공자와 소비자 간에 합의된 인터페이스가 시스템이 진화함에 따라 일관되게 유지되도록 보장합니다. OpenAPI 또는 Swagger 사양을 통해 정의되는 이 계약은 양측이 의존하는 공식적인 계약처럼 엔드포인트, 메서드, 스키마, 헤더 및 상태 코드와 같은 예상 요청 및 응답 구조를 설명합니다.

API 계약 테스트는 일반적으로 두 가지 형태를 띱니다. 소비자 중심 계약(CDC)은 마이크로서비스의 통합 실패를 방지하기 위해 소비자의 관점에서 기대치를 정의합니다. API 팀이 생성하는 공급자 정의 계약은 광범위한 유효성 검사를 위해 지원되는 모든 상호 작용을 다룹니다. 기능 테스트 또는 단위 테스트와 달리, 이러한 테스트는 기본 로직이 아닌 API 인터페이스에 엄격하게 초점을 맞춥니다.

API 계약 테스트의 중요성

개발 수명 주기에서 API 계약 테스트를 우선시해야 하는 이유는 무엇일까요? 작은 API 변경도 심각한 다운스트림 오류를 유발할 수 있습니다. 계약을 조기에 검증하면 서비스 간의 일관된 통신을 보장하고 문제가 프로덕션에 도달하기 훨씬 전에 방지할 수 있습니다.

API 계약 테스트의 주요 이점은 다음과 같습니다:

전자상거래 또는 SaaS와 같이 빠르게 변화하고 반복적인 환경에서 API 계약 테스트는 안정적이고 예측 가능하며 사용자 친화적인 애플리케이션을 제공하는 데 필수적입니다.

수동 API 계약 테스트의 과제

API가 증가함에 따라 API 계약 테스트를 수동으로 실행하는 것은 빠르게 비현실적이 됩니다. 요청을 수동으로 작성하고, 사양에 따라 응답을 확인하고, 헤더 또는 오류 코드를 검증하는 것은 느리고 오류가 발생하기 쉬우며 확장하기 어렵습니다.

수동 API 계약 테스트의 주요 과제는 다음과 같습니다:

이러한 제약 사항들은 빠르게 변화하고 신뢰할 수 있는 API 개발에 자동화된 API 계약 테스트가 왜 필수적인지 보여줍니다. 느리고 일관성 없는 수동 검사는 API 안정성에 대한 신뢰를 약화시킵니다.

Apidog에서 API 계약 테스트 시작하기

프로젝트에서 API 계약 테스트를 활용할 준비가 되셨나요? 포괄적인 API 개발 플랫폼인 Apidog는 내장된 스키마 유효성 검사 및 스크립팅 기능을 통해 이를 간소화하여 이상적인 시작점을 제공합니다.

Apidog에서 API 계약 테스트를 활성화하려면 OpenAPI 사양을 가져오거나 새 프로젝트를 생성하는 것부터 시작하세요. Apidog는 스키마에서 테스트를 자동 생성하여 설정을 간소화합니다.

Apidog에서 새 프로젝트 생성

실제 예제를 위해 API 탐색의 고전적인 예인 Apidog의 데모 Pet Store 프로젝트를 사용해 보겠습니다. Apidog를 실행하고 "Demo Pet" 프로젝트를 선택한 다음 "/pet/{petId}" GET 엔드포인트로 이동합니다 (참고: 쿼리는 "/get/pets/{id}"를 사용하지만, 표준 Petstore에 맞춰 "/pet/{petId}"입니다). 왼쪽 상단 드롭다운을 통해 환경을 "petstore env" 또는 "localmock"으로 설정하고,

환경 선택

요청을 실행합니다. 다음과 같은 응답을 받을 수 있습니다:

{
  "id": 1,
  "category": {
    "id": 1,
    "name": "dogs"
  },
  "name": "doggie",
  "photoUrls": [],
  "tags": [],
  "status": "available"
}

이는 계약 유효성 검사의 단계를 설정합니다.

"Test Cases" 탭으로 이동하여 "Add Case"를 클릭하여 새 스위트를 만듭니다.

테스트 케이스 추가

전처리 섹션에서 JSON 스키마와 변수를 정의하는 사용자 지정 스크립트를 추가합니다.

단계 1: 전처리기로 이동

Apidog에서 사용자 지정 테스트 스크립트 생성

단계 2: 사용자 지정 JS 코드 추가

Apidog에 사용자 지정 JS 테스트 스크립트 코드 추가
// petId가 설정되지 않은 경우 설정 (문자열로)
if (!pm.environment.get("petId")) {
  pm.environment.set("petId", "1");
}

// pet 응답에 대한 JSON 스키마 정의
const petSchema = {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "id": { "type": "integer" },
    "category": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" }
      },
      "required": ["id", "name"],
      "additionalProperties": true
    },
    "name": { "type": "string" },
    "photoUrls": {
      "type": "array",
      "items": { "type": "string", "format": "uri" },
      "minItems": 0
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" }
        },
        "required": ["id", "name"],
        "additionalProperties": true
      },
      "minItems": 0
    },
    "status": {
      "type": "string",
      "enum": ["available", "pending", "sold"]
    }
  },
  "required": ["id", "name", "photoUrls", "status"],
  "additionalProperties": true
};

// 환경 변수에 스키마 저장 (문자열로 변환)
pm.environment.set("pet_schema", JSON.stringify(petSchema));

// (선택 사항) 디버깅을 위해 콘솔에 로그 출력
console.log("Pre-processor: petId =", pm.environment.get("petId"));

다음으로, 후처리기에 유효성 검사 스크립트를 붙여넣습니다.

사용자 지정 후처리기 스크립트 추가
// 스키마 유효성 검사를 위해 AJV 사용
var Ajv = require('ajv');
var ajv = new Ajv({ allErrors: true, logger: console });

// 환경에서 스키마 검색
var raw = pm.environment.get("pet_schema");
var schema;
try {
  schema = (typeof raw === 'string') ? JSON.parse(raw) : raw;
} catch (err) {
  pm.test('스키마가 유효한 JSON입니다', function() {
    pm.expect(false, 'pet_schema는 유효한 JSON이 아닙니다: ' + err.message).to.be.true;
  });
  // 추가 테스트 중지
  return;
}

// 응답 본문을 JSON으로 파싱
var responseData;
try {
  responseData = pm.response.json();
} catch (err) {
  pm.test('응답이 유효한 JSON입니다', function() {
    pm.expect(false, '응답 본문이 JSON이 아닙니다: ' + err.message).to.be.true;
  });
  return;
}

// 상태 코드 테스트
pm.test('상태 코드는 200입니다', function() {
  pm.expect(pm.response.status).to.eql("OK");
});

// 스키마 유효성 검사
pm.test('응답이 pet 스키마와 일치합니다', function() {
  var valid = ajv.validate(schema, responseData);
  if (!valid) {
    console.log('AJV 오류:', ajv.errors);
  }
  pm.expect(valid, '응답이 스키마와 일치하지 않습니다. 오류는 콘솔을 참조하세요').to.be.true;
});

// 추가 어설션
pm.test('반환된 id가 요청된 petId와 일치합니다', function() {
  var requested = pm.environment.get("petId");
  // petId는 문자열로 저장되지만, 응답의 id는 정수입니다
  var requestedNum = Number(requested);
  if (!isNaN(requestedNum)) {
    pm.expect(responseData.id).to.eql(requestedNum);
  } else {
    pm.expect(String(responseData.id)).to.eql(String(requested));
  }
});

pm.test('Name은 문자열입니다', function() {
  pm.expect(responseData.name).to.be.a('string');
});

pm.test('Status는 예상 값 중 하나입니다', function() {
  pm.expect(responseData.status).to.be.oneOf(['available', 'pending', 'sold']);
});

// 선택 사항: 더 자세한 확인 (category, photoUrls, tags)
pm.test('Category에 id와 name이 있습니다', function() {
  pm.expect(responseData.category).to.have.property('id');
  pm.expect(responseData.category).to.have.property('name');
});

pm.test('최소 하나의 사진 URL', function() {
  pm.expect(responseData.photoUrls).to.be.an('array').that.is.not.empty;
});

pm.test('Tags는 유효한 객체입니다', function() {
  pm.expect(responseData.tags).to.be.an('array');
  if (responseData.tags.length > 0) {
    responseData.tags.forEach(function(tag) {
      pm.expect(tag).to.have.property('id');
      pm.expect(tag).to.have.property('name');
    });
  }
});

"Run"을 눌러 실행합니다. Apidog는 오른쪽에 "Passed" 또는 "Failed" 결과를 확장 가능한 세부 정보와 함께 표시합니다. 성공적인 실행은 다음과 같습니다.

API 응답:

API 응답
{
  "id": 1,
  "category": {
    "id": 1,
    "name": "dog"
  },
  "name": "Jasper",
  "photoUrls": [
    "https://loremflickr.com/400/400?lock=7187959506185006"
  ],
  "tags": [
    {
      "id": 3,
      "name": "Yellow"
    }
  ],
  "status": "available"
}

통과된 테스트:

  1. 상태 코드는 200입니다
  2. 응답이 pet 스키마와 일치합니다
  3. 반환된 ID가 요청된 petId와 일치합니다
  4. 이름은 문자열입니다
  5. 상태는 예상 값 중 하나입니다
  6. Category에 id와 name이 있습니다
  7. 최소 하나의 사진 URL
  8. Tags는 유효한 객체입니다
API 계약 테스트 결과 보기

실패를 시뮬레이션하려면 상태 코드 테스트를 "OK" 대신 숫자(200)를 예상하도록 변경하고,

JS 테스트 스크립트 변경

다시 실행하여 어설션 오류를 확인합니다.

어설션 오류

회귀 실행을 위해 스위트를 저장합니다. 스키마 검사를 위한 AJV 통합을 갖춘 Apidog의 직관적인 인터페이스는 API 계약 테스트를 대중화하여 복잡한 유효성 검사를 일상적인 작업으로 만듭니다.

자주 묻는 질문

Q1. API 계약 테스트와 통합 테스트의 차이점은 무엇인가요?

답변: API 계약 테스트는 비즈니스 로직을 실행하지 않고 인터페이스 계약의 유효성을 검사하는 반면, 통합 테스트는 데이터 흐름 및 종속성을 포함하여 서비스가 상호 작용하는 방식을 검사합니다.

Q2. API 계약 테스트를 GraphQL API에 적용할 수 있나요?

답변: 네, 주로 REST용으로 설계되었지만 Pact와 같은 도구는 GraphQL 스키마를 지원하며 쿼리/응답 구조 및 변형에 중점을 둡니다.

Q3. CI/CD 파이프라인에서 API 계약 테스트는 얼마나 자주 실행되어야 하나요?

답변: 이상적으로는 문제를 조기에 파악하기 위해 모든 커밋 또는 풀 리퀘스트 시에 실행되어야 하며, 포괄적인 커버리지를 위해 야간 실행도 권장됩니다.

Q4. 팀에 계약 테스트를 위한 OpenAPI 사양이 없는 경우는 어떻게 해야 하나요?

답변: Swagger Codegen과 같은 도구를 사용하여 기존 코드에서 사양을 생성하는 것부터 시작한 다음, 협업을 통해 기준을 설정하도록 개선해야 합니다.

Q5. API 계약 테스트가 레거시 API에도 적합한가요?

답변: 물론입니다. 현재 동작을 문서화하기 위해 사양을 개조한 다음, 현대화 과정에서 회귀를 방지하기 위해 테스트를 자동화해야 합니다.

결론

우리의 탐구를 마무리하면서, API 계약 테스트가 상호 연결된 세상에서 신뢰할 수 있고 확장 가능한 API의 핵심임을 알 수 있습니다. 수동 작업의 고통을 줄이는 것부터 자동화된 안전장치를 강화하는 것까지, API 계약 테스트는 팀이 두려움 없이 혁신할 수 있도록 지원합니다. Apidog와 같은 도구를 활용하여 실무를 개선하고, 애플리케이션이 회복력과 효율성을 얻는 것을 지켜보십시오. 기존 계약을 개선하든 새로운 계약을 만들든, 우수한 API 거버넌스를 향한 길은 단일하고 잘 정의된 테스트에서 시작됩니다.

💡
아름다운 API 문서를 생성하는 훌륭한 API 테스트 도구를 원하십니까?

개발팀이 최대한의 생산성을 가지고 함께 작업할 수 있는 통합된 올인원 플랫폼을 원하십니까?

Apidog는 모든 요구사항을 충족하며, 훨씬 더 저렴한 가격으로 Postman을 대체합니다!
버튼

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요