CI에서 OpenAPI 스펙 비교 및 브레이킹 체인지 방지 방법

병합 전에 두 OpenAPI 스펙 버전을 비교하여 호환성을 손상하는 API 변경 사항을 감지하세요. CI에서 oasdiff 또는 openapi-diff를 사용한 다음, Apidog CLI로 계약 불일치를 해소하세요.

Ashley Goolam

Ashley Goolam

16 June 2026

CI에서 OpenAPI 스펙 비교 및 브레이킹 체인지 방지 방법

Apidog 엔터프라이즈

온프레미스 배포

SSO & RBAC

SOC 2 준수

Apidog Enterprise 살펴보기

풀 리퀘스트가 openapi.yaml을 수정합니다. CI 검사는 통과됩니다. 스펙은 유효하고, 린트 검사를 통과하며, 두 명의 검토자가 승인합니다. 3일 후 모바일 클라이언트에서 이전에 존재했던 응답 필드가 사라져서 널 포인터 크래시가 발생하기 시작합니다. 아무도 의도적으로 제거하지 않았습니다. 리팩터링 중에 누군가 속성 이름을 변경했지만, 검토 과정에서 아무도 이를 알아채지 못했습니다.

이는 일반적인 유효성 검사기가 결코 알아차리지 못하는 간극입니다. 스펙은 완벽하게 잘 구성되어 있어도, 그 스펙에 의존하는 모든 소비자(클라이언트)를 망가뜨릴 수 있습니다. 이를 알 수 있는 유일한 방법은 새 스펙을 교체하는 이전 버전과 변경 사항별로 비교하여 "이것이 어제까지 작동하던 클라이언트를 망가뜨릴까?"라는 질문을 하는 것입니다. 이러한 비교가 OpenAPI diff이며, 이를 병합 게이트로 실행하는 것은 API 리포지토리에 추가할 수 있는 가장 높은 투자 수익률을 가진 검사 중 하나입니다.

버튼

OpenAPI Diff가 실제로 비교하는 것

OpenAPI diff는 두 개의 스펙(기준 스펙과 헤드 스펙)을 가져와 그들 사이의 변경 사항을 보고합니다. 기준 스펙은 일반적으로 대상 브랜치에 있는 스펙(실제 운영 중인 스펙)이고, 헤드 스펙은 풀 리퀘스트에서 제안하는 스펙입니다. 좋은 diff 도구는 git diff처럼 단순히 텍스트 델타를 출력하지 않습니다. OpenAPI 구조를 이해하여 표면적인 편집과 계약을 파기하는 변경 사항을 구별할 수 있습니다.

여기서 중요한 차이점이 있습니다. 일부 변경 사항은 추가적이고 안전합니다:

기존 클라이언트들은 이 모든 변경 사항에도 불구하고 계속 작동합니다. 그들은 항상 보내던 것을 보내고 항상 읽던 것을 읽습니다. 다른 변경 사항들은 하위 호환성을 파괴하며, 이는 문제가 되는 변경 사항입니다:

OpenAPI diff 도구의 역할은 두 문서의 모든 경로, 파라미터, 스키마, 응답을 스캔하고 각 변경 사항을 위 버킷 중 하나로 분류하는 것입니다. 이 분류가 핵심입니다. 원시적인 라인 diff는 50줄의 재포맷 아래에 제거된 required 필드를 숨기지만, 구조적 diff는 이를 호환성이 깨지는 변경 사항으로 드러내고 어떤 경로에 존재하는지 알려줍니다.

일부 변경 사항이 호환성을 깨고 다른 변경 사항이 그렇지 않은 이유에 대한 근본적인 정신 모델을 알고 싶다면, API를 대규모로 버전 관리하고 폐기하는 방법에 대한 가이드에서 호환성 규칙을 자세히 다룹니다. diff 도구는 검토자가 이 규칙을 기억하기를 바라는 대신, 기계적으로 이 규칙을 적용하는 방법입니다.

oasdiff: 오픈소스 핵심 도구

oasdiff는 대부분의 팀이 사용하는 오픈소스 도구입니다. 단일 Go 바이너리이며, 빠르고, 특히 호환성 파기 변경 질문을 중심으로 구축되었습니다. OpenAPI 3.0 및 3.1 문서를 읽고 비교에서 원하는 결과에 따라 몇 가지 하위 명령을 제공합니다.

가장 많이 사용될 세 가지:

병합 게이트의 경우, breaking이 중요합니다. 이를 기준 스펙과 헤드 스펙에 지정합니다:

oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR

base-openapi.yaml은 대상 브랜치의 스펙이고 head-openapi.yaml은 풀 리퀘스트의 스펙입니다. breaking 하위 명령은 호환되지 않는 변경 사항만 출력합니다. --fail-on ERR 플래그는 이를 게이트로 전환하는 역할을 합니다: ERR 수준으로 분류된 변경 사항을 발견하면 명령이 0이 아닌 상태로 종료됩니다. 0이 아닌 종료는 CI가 실패로 읽는 보편적인 신호입니다.

이 심각도 모델을 이해하는 것이 중요합니다. oasdiff는 호환성 파기 변경 사항을 여러 수준으로 분류하며, ERR은 클라이언트를 망가뜨릴 심각한 변경 사항입니다. WARN은 작성 방식에 따라 일부 클라이언트를 망가뜨릴 수 있는 변경 사항을 다루고, INFO는 정보 제공용입니다. 어디서 선을 그을지는 여러분이 결정합니다. --fail-on ERR은 명확한 호환성 파기만 차단합니다. --fail-on WARN은 더 엄격하며 불확실한 경우도 잡아냅니다.

합격/실패 여부가 아니라 변경 로그나 PR 댓글을 위한 읽기 쉬운 요약을 원한다면, changelog 하위 명령이 더 친숙한 출력을 제공합니다:

oasdiff changelog base-openapi.yaml head-openapi.yaml

oasdiff는 몇 가지 정말 유용한 기능이 있습니다. 경로 파라미터 이름이 변경되어도 엔드포인트 매칭이 가능하여, {userId}{id}로 변경될 때 경로가 동일하다면 삭제-추가로 플래그를 지정하지 않습니다. 비교 전에 allOf 스키마를 병합하여 상속으로 인한 노이즈가 발생하지 않도록 할 수 있습니다. 그리고 일반 텍스트 이상의 것을 출력합니다: HTML, JSON, YAML, Markdown이 모두 출력 플래그를 통해 제공되므로, 결과를 CI 주석이나 생성된 변경 로그에 쉽게 통합할 수 있습니다. 5분 안에 파이프라인에 통합하고 호환성 파기 변경 사항에 대해 보수적으로 신뢰할 수 있는 도구로는 이만한 것이 없습니다.

openapi-diff: JVM 대안

스택이 이미 JVM 기반이라면, OpenAPITools/openapi-diff는 견고한 두 번째 옵션이며 알아둘 가치가 있습니다. Java 기반 도구(Java 8 이상)로 두 개의 OpenAPI 3.x 스펙을 비교하고 차이점을 HTML, Markdown, AsciiDoc, JSON 또는 콘솔 텍스트로 렌더링합니다. 빌드된 jar, Maven, Homebrew 또는 Docker 이미지를 통해 실행할 수 있어 다양한 빌드 설정에 큰 어려움 없이 적용됩니다.

이 도구의 비교는 파라미터, 응답, 엔드포인트 및 HTTP 메서드 깊이까지 들어가며, 모두가 중요하게 여기는 동일한 기준을 제시합니다: 하위 호환성을 유지한 변경 사항과 이를 파괴한 변경 사항. CLI는 간단합니다:

openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible

--fail-on-incompatible 플래그는 변경 사항이 하위 호환성을 파괴했을 때만 0이 아닌 값으로 종료되며, 이는 정확히 원하는 게이트 동작입니다. 어떤 변경 사항이라도 실패시키고 싶다면 더 엄격한 --fail-on-changed가 있으며, 스크립트에서 한 단어 답변을 원할 때 no_changes, compatible, incompatible만 출력하는 --state 모드도 있습니다.

이 도구의 장점은 렌더링된 출력입니다. HTML 및 Markdown 보고서는 깔끔하고 상세하여, 단순히 CI 종료 코드가 아닌 사람이 실제로 읽을 수 있는 diff 아티팩트를 원할 때 openapi-diff를 강력한 선택지로 만듭니다. 단점은 JVM 의존성과 Go 바이너리보다 무거운 시작 시간입니다. 팀이 이미 Java 기반이라면 이 비용은 0이며 도구는 바로 통합됩니다. 그렇지 않다면 oasdiff가 더 가볍습니다. 둘 다 호환성 파기 변경 질문에 잘 답변하며, 이미 유지하고 있는 런타임에 맞는 것을 선택하세요.

CI에 diff를 병합 게이트로 연결하기

수동으로 실행하는 diff는 아무것도 잡아내지 못합니다. 왜냐하면 실행하는 것을 잊어버리는 순간이 바로 문제가 발생하여 배포되는 순간이기 때문입니다. 게이트는 파이프라인에 존재해야 하며 스펙을 건드리는 모든 풀 리퀘스트에서 실행되어야 합니다.

CI의 한 가지 난점은 두 가지 버전의 스펙이 동시에 필요하다는 것입니다: 대상 브랜치의 기준 스펙과 PR의 헤드 스펙. PR 체크아웃은 헤드 스펙을 제공합니다. 기준 스펙은 두 번째 체크아웃 없이 Git 히스토리에서 바로 가져옵니다:

name: openapi-diff
on:
  pull_request:
    paths:
      - "openapi.yaml"

jobs:
  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Get base spec
        run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
      - name: Install oasdiff
        run: |
          curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
      - name: Diff for breaking changes
        run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR

몇 가지 세부 사항이 여기서 중요합니다. fetch-depth: 0은 전체 히스토리를 가져와 git show가 기준 브랜치에 접근할 수 있도록 합니다. git show origin/<base>:openapi.yaml 라인은 대상 브랜치에 존재하는 스펙을 읽어 파일로 작성하며, 추가 클론이 필요 없습니다. paths 필터는 스펙이 실제로 변경될 때만 작업이 실행되도록 하여 관련 없는 PR이 비용을 지불하지 않도록 합니다. 그리고 마지막 단계는 게이트입니다: oasdiff breakingERR 수준의 변경 사항을 발견하면 0이 아닌 값으로 종료되고, 작업이 실패로 표시되며, 누구든지 병합 버튼을 클릭하기 전에 PR에 실패한 검사가 표시됩니다.

작성자는 코드가 아직 검토 중인 동안, 어떤 변경 사항이 어떤 경로에서 호환성을 파괴했는지 정확히 확인합니다. 이것이 전체 가치입니다. 고객의 크래시 보고서에서 발견되는 대신, 가장 저렴한 시점에 문제가 포착됩니다.

물론 모든 호환성 파기 변경 사항이 실수는 아닙니다. 때로는 의도적인 주요 버전을 출시하며 호환성 파기가 의도된 것일 수 있습니다. 깔끔한 패턴은 기본적으로 게이트를 설정하고 예외 상황에 대해서는 명시적인 재정의를 요구하는 것입니다: PR에 라벨을 붙이거나, info.version을 변경하거나, 별도의 승인된 워크플로우를 사용하는 것입니다. 이렇게 하면 호환성 파기는 항상 누군가가 의도적으로 내린 결정이며, 실수로 발생한 사고가 아닐 것입니다. API 버전 관리 전략 가이드는 호환성 파기가 새로운 주요 버전을 정당화하는 시점과 피해야 할 시점을 자세히 설명합니다.

Diff가 메울 수 없는 격차

위의 모든 도구의 한계점이며, 이는 중요한 부분입니다. diff는 두 파일을 비교합니다. 새 문서가 이전 문서와 하위 호환성이 있다고 알려줄 뿐입니다. 실행 중인 서비스가 실제로 둘 중 하나와 일치하는지 여부에 대해서는 아무것도 말해주지 않습니다.

이것은 다른 종류의 실패이며, 프로덕션에서 가장 큰 피해를 줍니다. 스펙은 created_at 필드를 약속하지만, 구현은 3 스프린트 전에 조용히 이를 반환하지 않게 되었습니다. 스펙은 엔드포인트가 200을 반환한다고 말하지만, 실제 서비스는 아무도 테스트하지 않은 조건에서 500을 반환합니다. 두 스펙 버전이 일치하기 때문에 diff는 깨끗합니다. 계약과 코드는 일치하지 않습니다. 정적 diff는 API와 통신하지 않으므로 이를 알 방법이 없습니다.

이러한 간극을 메우는 것은 계약 자체를 diff하는 것이 아니라 실제 API를 계약에 대해 테스트하는 것을 의미합니다. 스펙에서 테스트를 생성하고, 실행 중인 서비스에 대해 테스트를 실행하며, 실제 응답이 문서화된 형태와 일치하는지 확인합니다. 이것이 바로 계약 테스트이며, 이는 작성된 것과 실제로 배포된 것 사이의 불일치를 잡아내는 계층입니다.

Apidog 및 Apidog CLI로 격차 해소하기

Apidog는 이 루프를 위해 구축되었으며, 따라서 diff 단계의 대체품이 아니라 자연스러운 동반자입니다. OpenAPI 스펙을 Apidog 프로젝트로 가져오거나 동기화하면, Apidog는 스펙에서 직접 테스트 시나리오를 생성할 수 있으며, 스키마에서 파생된 어설션을 포함합니다. 테스트는 실제 응답이 문서화된 유형, 필수 필드 및 상태 코드와 일치하는지 확인합니다. 계약이 변경될 때마다 동기화되지 않는 병렬 테스트 스크립트를 수동으로 작성하는 대신, 이러한 시나리오를 시각적으로 구축하고 유지 관리합니다.

Apidog는 설계, 모킹, 테스트를 하나의 작업 공간에 유지하므로 스펙은 이 모든 것의 진정한 소스 역할을 합니다. Apidog를 다운로드하고 기존 스펙을 가져와 자신의 API에서 이 루프를 시도해 볼 수 있습니다. 처음부터 버전 간에 스펙을 어떻게 제어할지 고민 중이라면, Git으로 OpenAPI 스펙 버전 관리에 대한 안내서가 이 워크플로우와 잘 어울립니다.

Apidog CLI는 파이프라인에서 이러한 시나리오를 헤드리스로 실행하는 도구입니다. npm 패키지입니다:

npm install -g apidog-cli

ID로 시나리오를 실행하고, 유효성을 검사할 환경을 지정한 다음, CI 친화적인 보고서를 요청합니다:

apidog run \
  --access-token $APIDOG_ACCESS_TOKEN \
  -t <scenarioId> \
  -e <environmentId> \
  -r junit,cli \
  --out-dir ./apidog-reports

액세스 토큰은 실행을 인증하며, 커밋된 파일이 아닌 CI 비밀로 저장됩니다. -t 플래그는 시나리오를 선택하고, -e는 환경을 선택하며, -r junit,cli는 빌드 로그를 위한 읽기 쉬운 터미널 출력과 함께 CI 대시보드를 위한 기계 판독 가능한 JUnit XML을 내보냅니다. ID를 추측할 필요가 없습니다. 실제 시나리오 및 환경 ID가 이미 채워진 정확한 명령을 Apidog의 시나리오 CI/CD 탭에서 복사합니다. 모든 옵션 표면을 원한다면, 완전한 CLI 가이드에서 모든 플래그를 문서화하고 있으며, apidog run --help는 요청 시 이를 출력합니다.

게이트 동작은 diff와 동일한 원칙을 따릅니다. 라이브 응답이 더 이상 계약과 일치하지 않아 어설션이 실패하면 apidog run은 0이 아닌 값으로 종료됩니다. CI는 종료 코드를 읽어 단계를 실패로 표시하고 병합을 차단합니다. 추가 구성이 필요 없습니다. 실행 단계가 파이프라인에 있는 한, 계약 회귀는 호환성 파기 변경 diff와 동일한 방식으로 라인을 중단시킵니다.

전체 사전 병합 시퀀스

두 부분을 결합하면 두 가지 유형의 문제를 모두 잡아내는 파이프라인을 얻을 수 있습니다. diff는 스펙을 읽어 클라이언트를 망가뜨릴 수 있는 변경 사항을 잡아냅니다. 계약 테스트는 실행 중인 API를 실제로 사용하여 스펙을 더 이상 준수하지 않는 서비스를 잡아냅니다. 이를 별도의 작업으로 실행합니다:

jobs:
  breaking-changes:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
      - run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
      - run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR

  contract-conformance:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm install -g apidog-cli
      - name: Run contract tests
        run: |
          apidog run \
            --access-token "$APIDOG_ACCESS_TOKEN" \
            -t 605067 \
            -e 1629989 \
            -r junit,cli \
            --out-dir ./apidog-reports
        env:
          APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: apidog-report
          path: ./apidog-reports

두 작업은 병렬로 실행됩니다. diff 작업은 파일을 읽고 git만 필요하므로 몇 초 만에 완료됩니다. 적합성(conformance) 작업은 접근 가능한 환경이 필요하므로 일반적으로 배포된 스테이징 빌드를 대상으로 실행됩니다. 업로드 시 if: always()는 테스트가 실패하더라도 보고서가 계속 흐르도록 하는데, 이는 보고서를 읽고 싶을 때와 정확히 일치합니다. 두 작업 중 하나라도 실패하면 PR은 차단됩니다. 실제 파이프라인에서 CLI를 실행하는 방법에 대한 자세한 내용은 Apidog CLI GitHub Actions 가이드와 더 광범위한 CI/CD 파이프라인 가이드에서 자세히 설명합니다.

버튼

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

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