본문으로 건너뛰기
  1. 포스트/

API는 만들기 전에 설계하기: OpenAPI 우선 워크플로우

· loading · loading ·
인재덕
작성자
인재덕
서울에 거주하는 리더 겸 소프트웨어 엔지니어

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

어쩌다 시작했나
#

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

프로세스
#

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

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

도구 관련 메모
#

  • yaml 시각화는 Swagger Editor면 충분합니다. 파일을 올리면 제대로 된 Swagger 문서로 렌더링해서 보여 줍니다.
  • 이 스키마는 어디까지나 계획용 산출물입니다. 실제 API가 배포되면 각 서비스가 자기 openapi.json을 생성해야 하고, 그걸 내려받아 다음 계획에 다시 활용하면 됩니다.
  • CI/CD 파이프라인은 yaml을 받아서 목 서버를 띄울 수 있어야 합니다. openapi-mock이 쓸 만한 선택지로 보입니다.

왜 이 수고를 하나
#

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

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

부록 A - yaml 예제
#

openapi: 3.1.0
info:
  title: PKY Sprint 3
  description: |-
    PKY Sprint 3 Development Schema
  termsOfService:
  contact:
    email: jared@lynskey.co.nz
  license:
    name: None
    url:
  version: 1.0.11
externalDocs:
  description:
  url:
servers:
  - url: https://api-mock.com/api/v3
tags:
  - name: curation
    description: PKY-1081 Curated Lists Feature
    externalDocs:
      description: Jira Epic
      url: https://pickydev.atlassian.net/browse/PKY-1081
paths:
  /curations:
    get:
      tags:
        - curation
      summary: Return the curations created by users
      description: Returns a map of status codes to quantities
      operationId: getCurations
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: integer
                  format: int32
      security:
        - api_key: []
  /curations/{curationId}:
    get:
      tags:
        - curation
      summary: Find purchase order by ID
      description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.
      operationId: getOrderById
      parameters:
        - name: orderId
          in: path
          description: ID of order that needs to be fetched
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: successful operation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
            application/xml:
              schema:
                $ref: '#/components/schemas/Order'
        '400':
          description: Invalid ID supplied
        '404':
          description: Order not found
    delete:
      tags:
        - curation
      summary: Delete purchase order by ID
      description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
      operationId: deleteOrder
      parameters:
        - name: orderId
          in: path
          description: ID of the order that needs to be deleted
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '400':
          description: Invalid ID supplied
        '404':
          description: Order not found
components:
  schemas:
    Curation:
      type: object
      properties:
        id:
          type: integer
          format: int64
          examples: [10]
        curationId:
          type: integer
          format: int64
          examples: [198772]
        quantity:
          type: integer
          format: int32
          examples: [7]
        status:
          type: string
          description: Order Status
          examples: [approved]
          enum:
            - placed
            - approved
            - delivered
        complete:
          type: boolean
      xml:
        name: order
    ApiResponse:
      type: object
      properties:
        code:
          type: integer
          format: int32
        type:
          type: string
        message:
          type: string
      xml:
        name: '##default'
  requestBodies:
    Curation:
      description: Curation object that needs to be added
      content:
        application/json:
          schema:
            $ref:
        application/xml:
          schema:
            $ref:
  securitySchemes:
    petstore_auth:
      type: oauth2
      flows:
        implicit:
          authorizationUrl:
          scopes:
            write:curations: modify curations in your account
            read:curations: read your curations
    api_key:
      type: apiKey
      name: api_key
      in: header