↓ 본문으로 건너뛰기

API OpenAPI API Design

API 설계 우선(Design-First) 워크플로우: 스프린트마다 OpenAPI 계약 정하기

스프린트마다 돌리는 API 설계 우선 개발 프로세스. 백엔드와 프론트엔드 담당자가 OpenAPI 계약에 합의하면 CI가 목 서버를 띄우고, 양쪽이 동시에 개발합니다. YAML 예제 포함.

API 설계 우선(Design-First) 워크플로우: 스프린트마다 OpenAPI 계약 정하기

얼마 전 OpenAPI를 활용한 API 우선 개발에 대해 일반론을 썼는데, 이번 글은 그 실전편입니다. 우리 팀에서 시험 운영 중인 계획 워크플로우와, 거기에 딸린 YAML 예제를 소개합니다.

요약하면, 이 설계 우선 프로세스는 스프린트마다 다음 순서로 돌아갑니다.

  1. 그 스프린트의 API 작업을 맡을 담당자를 백엔드와 프론트엔드에서 한 명씩 정합니다.
  2. 두 사람이 스프린트 요구사항에 맞춰 OpenAPI 스키마를 작성(또는 수정)하고, 양쪽이 동의할 때까지 리뷰합니다.
  3. 스키마를 저장소에 커밋합니다. 이제부터 이게 계약입니다.
  4. 커밋된 스키마로 CI가 목 서버를 띄웁니다.
  5. 프론트엔드는 목 서버를 상대로 개발하고, 그동안 백엔드는 실제 엔드포인트를 구현합니다.
  6. 실제 서비스가 배포되면, 거기서 생성된 스펙이 다음 스프린트 스키마의 출발점이 됩니다.

어쩌다 시작했나
#

발단은 회고였습니다. API 계획이 얼마나 안 보이는지에 대해 피드백이 꽤 직설적이었거든요. 백엔드는 요구사항을 자기들 나름대로 해석해서 엔드포인트를 만들고, 프론트엔드는 응답 형태를 나중에야 알게 되고, 어긋난 부분은 통합 시점에, 그러니까 가장 비싼 순간에 터져 나왔습니다. 그래서 나온 제안이 양쪽을 계획 단계부터 끌어들이고, OpenAPI 스키마를 모든 것의 기준점으로 삼자는 것이었습니다.

프로세스
#

매주 백엔드 한 명, 프론트엔드 한 명, 이렇게 대표 두 명을 뽑습니다. 이들의 임무는 스프린트 요구사항을 이해하고 계획용 OpenAPI 스키마를 거기에 맞추는 것입니다. 둘이 함께 스키마를 리뷰하고 안 맞는 부분을 고치다 보면, 만들기 전에 양쪽 모두 정확히 뭘 만들지 알게 됩니다.

수정이 마무리되면 스키마를 저장소에 커밋하고, CI/CD가 그 yaml로 목 API를 생성합니다. 스프린트가 끝나면 다음 스프린트의 API 요구사항을 담은 새 yaml을 만들고, 같은 사이클을 반복합니다.

도구 관련 메모
#

yaml 시각화는 Swagger Editor면 충분합니다. 파일을 올리면 제대로 된 Swagger 문서로 렌더링해서 보여 줍니다.

CI/CD 파이프라인은 yaml을 받아서 목 서버를 띄울 수 있어야 합니다. Stoplight의 Prism이 널리 쓰이는 선택지이고, OpenAPI 3.1도 지원합니다.

npm install -g @stoplight/prism-cli
prism mock openapi.yaml

이렇게 하면 스키마의 모든 경로가 http://127.0.0.1:4010에서 서비스되고, 응답으로는 스키마의 examples가 그대로 돌아옵니다. 예를 들어 GET /lists/10을 호출하면 부록에 있는 예시 리스트가 옵니다. 계약도 제대로 지키게 합니다. 예제에서 API 키를 선언해 뒀기 때문에 X-API-Key 헤더가 없는 요청에는 401을 돌려주는데, 프론트엔드가 맞춰 개발해야 할 동작이 바로 이거죠. 컨테이너 안에서 돌릴 때는 바깥에서 접근할 수 있도록 -h 0.0.0.0을 붙이세요. openapi-mock은 응답을 무작위 데이터로 채워 주는, 좀 더 가벼운 대안입니다.

목 서버를 띄우기 전에 스키마를 린트해 두는 것도 좋습니다. 계약이 깨져 있으면 프론트엔드를 헷갈리게 하는 대신 빌드가 실패하니까요. Spectral은 한 줄짜리 룰셋만 있으면 바로 돌아갑니다.

npm install -g @stoplight/spectral-cli
echo 'extends: ["spectral:oas"]' > .spectral.yaml
spectral lint openapi.yaml

마지막으로, 이 스키마는 어디까지나 계획용 산출물입니다. 실제 API가 배포되면 각 서비스가 자기 openapi.json을 생성해야 하고, 그걸 내려받아 다음 계획에 다시 활용하면 됩니다.

왜 이 수고를 하나
#

논쟁이 가장 싼 곳으로 옮겨가기 때문입니다. 스키마를 쓰는 자리에 프론트엔드와 백엔드 엔지니어가 같이 앉아 있으면, 오해는 코드가 되기 전에 잡힙니다. API 요구사항은 스프린트 중간에 재협상되는 대신 한 번에 합의되고, 목 서버 덕분에 프론트엔드는 기다리지 않고 바로 시작하고, 중요한 디테일이 빠져나갈 틈도 훨씬 줄어듭니다. 두 쌍의 눈과 문서화된 계약을 다 통과해야 하니까요.

혁명적인 이야기는 하나도 없습니다. 그저 기계가 목으로 되돌려줄 수 있을 만큼 계획을 구체적으로 만들었을 뿐인데, 알고 보니 계획에 필요한 구체성이란 딱 그 정도더군요.

부록 A - yaml 예제
#

openapi: 3.1.0
info:
  title: MyApp Sprint 3
  description: Planning schema for sprint 3. Drives the mock server only.
  version: 0.3.0
servers:
  - url: https://api-mock.example.com/v1
tags:
  - name: lists
    description: Saved lists feature (PROJ-123)
security:
  - api_key: []
paths:
  /lists:
    get:
      tags: [lists]
      summary: Return the current user's saved lists
      operationId: getLists
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/List'
  /lists/{listId}:
    get:
      tags: [lists]
      summary: Find a list by ID
      operationId: getListById
      parameters:
        - name: listId
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/List'
        '404':
          description: List not found
components:
  schemas:
    List:
      type: object
      required: [id, title]
      properties:
        id:
          type: integer
          format: int64
          examples: [10]
        title:
          type: string
          examples: [Weekend reading]
        itemCount:
          type: integer
          examples: [7]
  securitySchemes:
    api_key:
      type: apiKey
      name: X-API-Key
      in: header