
나의 서비스에 Open API Generator를 적용했던 내용을 회고하는 글.
Open API Generator가 길어 OAG라고 축약하도록 하겠음.
https://github.com/Myunwoo/stash_frontend
https://github.com/Myunwoo/stash_backend
SI 프론트엔드 개발자로 일하면서 API는 호출할 대상일 뿐이었다. 그런데 항상 호출해오던 백엔드 시스템의 유지보수 업무를 회사가 수주하게 되어 백엔드 개발을 시작하게 되었다.
백엔드 소스 파악 중 OAG를 이용해 RestController의 interface와 DTO 자바 코드를 자동 생성하고 있는 것을 보게 되었다.
Open API Generator를 사용하는 것에 다른 장점이 많겠지만, 가장 크게 와 닿은 장점은
API Spec을 프론트엔드, 백엔드에서 일관되게 관리할 수 있다는 점
이었다. 그러나 우리 회사 프론트엔드 프로젝트에서는 아래와 같이 API를 호출하고 있었다.
// API에 enum처럼 번호를 부여(API_BFF_XXXX)
export const API_CMD: any = {
API_BFF_XXXX: { path: '/api/path', method: API_METHOD.GET, server: API_SERVER.TEST_SERVER, ...},
...
}
}
// RxJS를 이용해 직접 붙인 API 명으로 호출
combineLatest([
this.apiService.request(API_CMD.API_BFF_XXXX, ...),
]).subscribe({
next:(resp) => {
...
},
error:() => {
...
}
})
}
// this.apiService.request가 결국 OAG로 생성된 코드를 this.oagCall 내부에서 호출
public request(command: any, params: any, header?: any, ...): Observable<any> {
return this.oagCall(command, params, header, pathParams, version, isBypass)
}
OAG로 생성된 코드를 바로 호출하지 않는 것은 괜찮다고 생각한다.
API에 번호를 붙였기 때문에, 의사소통 시 API 이름을 이용해 효과적이었다.
하지만, 아래와 같은 단점이 있다.
따라서, 아래와 같은 조건을 지키며 사이드 프로젝트에 OAG를 적용해 보기로 하였다.
implementation 'org.openapitools:openapi-generator:5.1.1'
implementation 'org.openapitools:openapi-generator-gradle-plugin:5.1.1'
먼저 gradle에 의존성을 추가했다. yaml 파일을 이용해 컨트롤러와 DTO 코드를 자동 생성하게 될 텐데, API 성격에 따라 yaml을 분리할 계획이므로 plugin 의존성도 필요했다.
def stashSwaggerMap = [
"stash" : "stash.yaml",
"auth" : "auth.yaml",
"json" : "json.yaml"
]
stashSwaggerMap.each { entry ->
tasks.register("opnApiGenerate-stash-${entry.key}", org.openapitools.generator.gradle.plugin.tasks.GenerateTask) {
generatorName.set("spring")
inputSpec.set("$rootDir/src/main/resources/openapi_stash/${entry.value}")
outputDir.set(project.file("$buildDir/generated-sources").absolutePath)
apiPackage.set("com.ihw.stash.adapter.in.${entry.key}.web")
modelPackage.set("com.ihw.stash.adapter.in.${entry.key}.dto")
configOptions.set(
[
interfaceOnly : "true",
useBeanValidation : "true",
performBeanValidation : "true",
serializableModel : "true",
sourceFolder : "/java",
implFolder : "/java",
unhandledException : "true",
useTags : "true",
]
)
}
}
API 성격별로 yaml을 분리하고, yaml의 경로와 자동 생성될 코드의 위치를 build.gradle에 지정했다.
openapi: 3.0.0
info:
title: Stash FO api
version: 1.0.0
description: Stash FO api
servers:
- url: http://localhost:8080
tags:
- name: stash-controller
description: Stash api controller
paths:
/getStashDetail:
get:
tags:
- stash-controller
summary: API_STS_2001, Stash 상세 조회
description: Stash 상세 조회
operationId: getStashDetail
parameters:
- name: stashId
in: query
description: stash 아이디
required: true
schema:
type: integer
format: int64
responses:
'200':
description: 조회성공
content:
application/json:
schema:
$ref: './dto/StashDTO.yaml#/components/schemas/StashDetailOutDTO'
'400':
description: Invalid status value
src/resources/stash.yaml에 api 스펙을 위처럼 작성하고,
components:
schemas:
StashDetailOutDTO:
type: object
required:
- stash_id
- username
- title
properties:
stash_id:
type: integer
format: int64
username:
type: string
title:
type: string
description:
type: string
start_time:
type: string
end_time:
type: string
src/resources/dto/StashDTO.yaml에 DTO를 작성했다.
"scripts": {
...
"oag:gen:local": "(copy openapitools.local.json openapitools.json || cp openapitools.local.json openapitools.json) && openapi-generator-cli generate",
"oag:gen:prd": "(copy openapitools.prd.json openapitools.json || cp openapitools.prd.json openapitools.json) && openapi-generator-cli generate"
}
AWS에 프로젝트를 배포할 계획이었기 때문에, OAG 코드 생성 설정을 두 개의 json 파일로 분리하였다.
{
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.5.0",
"generators": {
"stash": {
"generatorName": "typescript-axios",
"templateDir": "#{cwd}/generate/template",
"output": "#{cwd}/generate/stash",
"inputSpec": "/Users/ihw/Documents/stash/src/main/resources/openapi_stash/stash.yaml"
},
...
}
}
}
pnpm oag:gen:local 명령어 실행 시 로컬에 저장된 yaml 파일에 접근해서 코드를 생성하도록 했다.
import type { CreateStashInDTO } from '@/generate/stash/api'
...
const createStash = async (data: CreateStashInDTO) => {
await StashControllerApi.createStash(data)
await fetchStashList()
}
stores에 위와 같이 코드 구현 후, 호출부에서 Typescript에 의한 정적 코드 검사가 이루어지도록 하였다. DTO 내부의 요소 검사는 예상대로 되지 않았지만, GET 파라미터 개수 변경 등에 대한 컴파일 시점 에러 발생은 정상적으로 확인되었다.
openapitools.local.json이 참조하는 yaml 파일은 내 컴퓨터의 경로이지만,
openapitools.prd.json이 참조하는 yaml 파일은 s3 경로로 지정했다.
백엔드 어플리케이션 배포 시, s3에 yaml 파일이 정적 배포 되도록 JenkinsFile을 작성했다.
pipeline {
agent any
environment {
...
OAG_DIR = '/var/lib/jenkins/workspace/stash_backend_deploy/src/main/resources/openapi_stash'
S3_BUCKET = '???'
}
stages {
stage('Checkout Source') {
...
}
stage('Build Application') {
...
}
stage('Upload to S3') {
steps {
echo 'Uploading specific files and directories to S3...'
sh """
aws s3 cp ${OAG_DIR} s3://${S3_BUCKET} --recursive --region ${AWS_REGION}
"""
}
}
stage('Deploy to EC2') {
...
}
}
post {
success {
echo 'Pipeline completed successfully.'
}
failure {
echo 'Pipeline failed. Please check the logs for more details.'
}
}
}
1인 개발자에게도 효율적인 Open API Generator
회사를 다니며 두 달 동안 느슨한 강도로 프로젝트를 진행해본 결과, Open API Generator를 사용함으로써 효율적인 작업이 가능했다.
대표적인 이유는 아래를 꼽을 수 있었다.
이상, 회고를 마친다. 다음 프로젝트에서 OAG를 사용하게 된다면 mustache 파일을 커스텀하여 어노테이션을 풍부하게 사용해 보고 싶은 욕심이 남았다.
관련 문서: https://openapi-generator.tech/docs/templating/?utm_source=chatgpt.com