Code-First에서 Contract-First로 : OpenAPI와 AsyncAPI

역사는 반복된다 – WSDL에서 REST, 그리고 다시 계약으로

현대의 소프트웨어 아키텍처는 수많은 마이크로서비스와 외부 시스템이 얽힌 거대한 복잡한 생태계를 구성하고 있습니다. HTTP 기반 웹 환경에서 API(Application Programming Interface)는 가장 널리 쓰이는 통신 방식으로, 복잡한 디지털 인프라 환경에서 시스템 간 연결을 책임지는 핵심 수단입니다.

API를 설계하고 관리하는 방식을 돌아보면, 흥미롭게도 역사가 반복되고 있음을 알 수 있습니다.

2000년대 초중반까지, SOA(Service-Oriented Architecture) 시절의 주류 기술은 SOAP(Simple Object Access Protocol)과 WSDL(Web Services Description Language)이었습니다. 어쩌면 이 기술이 원조 ‘Contract-First’ 라고 할 수 있습니다. WSDL이라는 엄격한 XML 규격 파일 하나만 있으면, 이기종간의 통신을 위한 스텁(Stub) 코드를 자동으로 생성할 수 있었고, 이를 통해 계약 기반의 프로토콜을 설계하는 서비스를 지향할 수 있었습니다. 하지만 SOA 방식의 무거운 페이로드와 경직된 스펙은 결국 더 가볍고 유연한 REST(Representational State Transfer) 기반의 웹 서비스로 생태계가 대거 이주하게 되었습니다.


Code-First의 장점과 한계

초기 REST 기반 시스템에서는 백엔드 코드가 중심이 되고, 문서는 사후에 생성되거나 수동으로 관리되는 경우가 많았습니다. Swagger와 OpenAPI 생태계는 이런 문제를 줄이기 위해 등장했으며, 코드 주석이나 어노테이션을 기반으로 문서를 자동 생성하고 테스트에 활용할 수 있게 했습니다.

이런 방식은 빠른 프로토타이핑에는 강점이 있지만, 요구사항 변화가 잦아질수록 문서와 실제 코드가 어긋나는 문제가 생기기 쉽습니다. 특히 백엔드 구현이 먼저 진행되면 프론트엔드와 다른 팀은 실제 계약을 확인하기 어려워지고, 그 결과 연동 지연이나 스펙 불일치가 발생하는 등 다양한 기술 부채가 쌓일 수 있게됩니다.

즉, Code-First는 나쁜 방식이 아니라, 상황에 따라 장점이 분명한 방식입니다. 다만 서비스 수가 늘고 팀 간 협업이 복잡해질수록, 문서와 구현의 차이를 지속적으로 통제하기가 어려워진다는 점이 한계로 드러납니다.


MSA가 계약을 더 중요하게 만든 이유

마이크로서비스 아키텍처에서는 서비스 간 호출이 잦고, 각 서비스의 경계가 더 명확해야 합니다. 이런 환경에서는 작은 스펙 변경도 다른 서비스에 연쇄 영향을 줄 수 있으므로, 인터페이스를 먼저 합의하는 계약 중심 접근이 특히 유리합니다.

Contract-First는 이런 문제를 줄이는 데 도움이 됩니다. 팀 간에 OpenAPI 같은 기계 판독 가능한 명세를 먼저 공유하면, 프론트엔드와 백엔드가 병렬로 개발을 진행할 수 있고, 변경사항도 계약을 기준으로 논의할 수 있습니다. 거대한 시스템을 작은 단위로 나눌수록, 경계와 책임을 먼저 정의하는 일이 중요해집니다.

현대적인 클라우드 네이티브 환경에서 API는 시스템 간의 단순한 통신 수단을 넘어, 비즈니스의 핵심 규약으로 볼 수 있습니다. 팀 간의 병목 현상과 이해 상충은 API 계약을 최우선으로 정의하는 컨트랙트 퍼스트(Contract-First)로 커뮤니케이션 표준화 문제를 해결할 수 있게 되었습니다. 특히 거대한 레거시 시스템을 마이크로 서비스로 분리할 때, API 계약을 먼저 정의하고 서비스 간의 명확한 경계(Boundary)를 설정하여 시스템을 안정적으로 현대화 할 수 있습니다.

<표 : API 개발 패러다임 비교>

구분Code-FirstContract-First
작업 순서코드 구현 → 문서 생성API 설계 정의 → 코드 생성/구현
팀 간 협업백엔드 완료 전까지 타 팀 대기설계도 공유 후 프론트/백엔드 병렬 작업
의사소통구두 협의 및 파편화된 문서 의존표준 명세서(OAS) 기반 소통
시스템 모던화기존 코드 변경에 따른 리스크 높음경계 설정을 통한 점진적 MSA 전환

설계 : OAS 명세화 및 단일 진실 공급원 구축

Contract-First의 첫 단계는 기획, 프론트엔드, 백엔드 담당자가 함께 OpenAPI Specification(OAS)을 작성하는 것입니다. 이때 Apicurio 같은 도구는 명세를 시각적으로 편집하고 함께 조율하는 데 유용합니다. 물론  YAML을 직접 편집하거나 코드 어노테이션 기반으로 문서를 보완하는 방식으로도 설계를 할 수 있습니다.

중요한 건 도구 자체보다도, 명세를 단일 진실 공급원처럼 다루는 태도입니다. 즉, 문서가 먼저 있고, 구현은 그 문서를 따라가도록 만드는 것입니다.

openapi: 3.0.3
info:
  title: User Service API
  version: 1.0.0
paths:
  /users/{id}:
    get:
      summary: 사용자 정보 조회
      parameters:
      – name: id
        in: path
        required: true
        schema: type: string
      responses:
        ‘200’:
          description: 정상 응답
          content:
            application/json:
              schema:
                $ref: ‘#/components/schemas/UserResponse’
components:
  schemas:
    UserResponse:
      type: object
      properties:
        id:
          type: string
        name:
          type: string

OpenAPI 명세서는 백엔드 구현, 프론트엔드 통신, 자동 테스트, 그리고 클라이언트 SDK 생성을 주도하는 단 하나의 진실의 원천으로, 불변의 생성 유물(Immutable generated artifacts)로 취급됩니다.

⧭ API 설계 4단계 프로세스

  1. API 메타데이터 정의: API의 명칭(예: To-do List API), 버전, 라이선스 정보를 입력하여 기본 골격을 잡습니다.
  2. 데이터 모델(Schema) 정의: API가 주고받을 데이터(id, title, completed, description, due date 등)를 설정합니다. 
  3. 경로(Path) 및 작업 설정: 사용자가 접근할 엔드포인트(예: /users)와 수행할 동작(GET, POST, DELETE 등)을 시각적으로 배치합니다.
  4. 응답 코드(Response) 구성: 성공(200 OK)이나 실패(404 Not Found) 시 반환할 데이터 형식과 보안 요구사항을 정의합니다.

설계도가 완성되었다면, 이제 실제 서버가 없어도 마치 있는 것처럼 API를 호출해 볼 차례입니다.


구현 : 명세가 코드를 이끄는 병렬 개발

명세가 확정되면 프론트엔드와 백엔드는 서로를 기다릴 필요 없이 병렬 개발에 들어갈 수 있습니다. 프론트엔드 개발자는 백엔드 코드가 없어도 Mock 서버를 띄워 화면과 흐름을 먼저 만들 수 있고, 백엔드는 OpenAPI 명세에 맞는 인터페이스와 DTO를 기준으로 구현을 시작할 수 있습니다.

① 프론트엔드: Mock 서버 구동 및 클라이언트 SDK 자동 생성

프론트엔드 개발자는 백엔드 코드 없이도 작업을 시작할 수 있습니다. fakeit이나 prism 같은 도구에 명세서를 넘기면 로컬 Mock API 서버가 즉시 구동됩니다.

# FakeIt을 이용한 Mock 서버 즉시 구동
$ fakeit –spec openapi.yaml –port 7080

② 백엔드 (Spring Boot): 불변의 인터페이스 생성

Spring Boot 환경에서는 OpenAPI Generator나 springdoc-openapi 계열 도구를 활용해 인터페이스와 모델을 생성할 수 있고, 빌드 파이프라인에  openapi-generator-maven-plugin을 추가하여, 빌드하면 api.yaml을 읽어 Spring의 인터페이스(Interface)와 DTO 객체가 자동으로 생성됩니다.

핵심은 생성된 코드의 불변성(Immutability)입니다. 자동 생성된 파일들은 src/main/gen과 같은 폴더에 격리되며 개발자가 임의로 수정해선 안 됩니다. 개발자가 직접 작성해야 하는 Controller 클래스는 .openapi-generator-ignore 파일에 등록하여 재생성 시 덮어써지는 것을 방지합니다.

public interface UsersApi {
    ResponseEntity<UserResponse> getUserById(String id);
}

@RestController
public class UsersApiController implements UsersApi {
    @Override
    public ResponseEntity<UserResponse> getUserById(String id) {
        return ResponseEntity.ok(new UserResponse(id, “홍길동”));
    }
}

테스트와 검증 : 계약을 지키는지 확인하는 검증

구현이 끝났다면 작성된 코드가 초기 계약을 정확히 준수하는지 검증해야 합니다. 이때 OpenAPI 명세를 기반으로 자동 테스트를 생성하는 도구를 활용하면, 문서와 구현이 어긋나는 문제를 훨씬 빨리 발견할 수 있습니다.

이 단계에서 Python 기반의 Schemathesis 같은 강력한 자동화 테스팅 도구를 활용할 수 있습니다.

Schemathesis는 개발자가 직접 테스트 코드를 작성할 필요 없이, OpenAPI 명세서를 분석하여 기계적으로 수백, 수천 개의 테스트 케이스를 자동 생성합니다. 단순히 500 내부 서버 에러가 발생하지 않는지를 확인하는 기본 검사뿐만 아니라, 명세에 정의된 HTTP 상태 코드(Status Code), 콘텐츠 타입(Content-Type), 그리고 응답 데이터 스키마(Response Schema)를 서버가 정확히 지키고 있는지 검증합니다. 여기에 사용자 정의 PyTest 로직을 결합하면, 모든 엔드포인트의 응답 시간이 1초 이내인지 체크하는 성능 테스트까지 파이프라인에 쉽게 통합할 수 있습니다.

# 로컬에 띄워진 서버를 대상으로 스펙 일치 여부 자동 테스트
$ schemathesis run openapi.yaml –base-url http://localhost:8080 –checks all

운영과 보안 : OpenAPI 에서 AsyncAPI로 확장

보안 역시 코드 내부가 아니라 설계 단계에서 계약으로 명시해야 합니다. OpenAPI 스펙 내부의 securitySchemes에 OAuth 2.0이나 OpenID Connect(OIDC) 기반 보안 방식을 정의하고, 각 엔드포인트에 필요한 인증 요구사항을 연결할 수 있습니다. 이렇게 하면 인증과 인가 정책도 API 계약의 일부가 됩니다.

그리고 시스템이 이벤트 주도 아키텍처(EDA)로 확장되면, REST만으로는 부족해집니다. Kafka, RabbitMQ 같은 메시지 브로커 안을 흐르는 이벤트는 요청-응답 방식과는 다른 계약이 필요합니다. 이때 OpenAPI의 철학을 비동기 환경에 맞게 확장한 것이 AsyncAPI입니다.

AsyncAPI는 어떤 서비스가 어떤 채널을 통해 메시지를 발행하거나 구독하는지, 그리고 그 페이로드 구조가 무엇인지를 명확히 정의합니다. 즉, 비동기 메시징 환경에서의 계약 문서라고 볼 수 있습니다.

asyncapi: 3.0.0
info:
  title: Order Event Stream
  version: 1.0.0
servers:
  production:
    host: broker.mycompany.com
    protocol: kafka
channels:
  order.created:
    address: order.created
    messages:
      orderCreated:
        payload:
          type: object
          properties:
            orderId:
              type: string
            amount:
              type: number
            createdAt:
              type: string
              format: date-time
operations:
  receiveOrderCreated:
    action: receive
    channel:
      $ref: ‘#/channels/order.created’

구현은 바뀌어도, 계약은 남는다

IT 기술은 끊임없이 변화합니다. 오늘은 Spring Boot로 작성된 서비스가 당장 내일 Go나 Node.js로 다시 작성될 수도 있고, 이기종 및 수많은 마이크로서비스에서 서로 호출 될 수 있습니다. 클라우드 제공자가 바뀌고, 메시지 브로커가 교체되고, 데이터베이스가 마이그레이션 됩니다. 이런 모든 변화 속에서 시스템 간의 ‘계약’이 기계가 읽을 수 있는 명세(Machine-readable Contract)로 정의되어 있다면, 내부 구현 언어나 프레임워크가 통째로 바뀌더라도 전체 서비스 생태계와 연동의 안정성은 흔들리지 않습니다.

Contract-First는 개발 방식의 전환이 아니라, 시스템 설계의 패러다임 전환입니다. 설계의 주도권을 코드에서 명세로 옮기고, 구현의 자유를 틀 안에서 행사하며, 변화에 강인한 아키텍처를 만드는 길입니다. OpenAPI와 AsyncAPI라는 훌륭한 도구를 적극적으로 활용하여, 병목을 없애고 인프라 전체에 예측 가능한 질서를 부여하는 진정한 마이크로서비스 아키텍처를 경험해 보시길 권장합니다.