얼마 전 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
