-
OpenAPI Specification으로 API-First 개발하기프로젝트/선착순 쿠폰 발급 시스템 2023. 5. 18. 00:01728x90
개요
OpenAPI Specification 3.0을 기준으로 문서의 스펙을 작성하고 이를 토대로 문서와 코드를 생성해보고자 합니다.
이를 위한 여러 가지 개념들을 파악하고 Swagger RestDocs등과는 어떻게 다른지 알아보고자 합니다.
더 나아가서 OpenAPI Specification으로 문서와 코드를 만들어봅니다.
OpenAPI Specification이란 무엇일까?
공식문서는 다음과 같이 말합니다.
OAS는 HTTP API에 대한 프로그래밍 언어에 독립적입니다.
컴퓨터와 사람 모두 이해할 수 있으며 최소한의 구현 논리로 API 소비자가 이해할 수 있도록 상호작용할 수 있습니다.
OpenAPI 와 Open API의 차이점
Open API는 단어 그대로 "개방된 API"입니다.
즉, 누구나 사용할 수 있도록 API의 endpoint가 개방되어 있다면 Open API입니다.
예를 들어 기상청 단기예보 조회 API, 우체국의 우편번호 API 등이 있습니다.
OpenAPI는 OpenAPI Specification(OAS)라고 부르며, Restful API를 기 정의된 규칙에 맞게 API spec을 json이나 yaml으로 표현하는 방식을 의미합니다.
OpenAPI와 Swagger
과거에는 OpenAPI2.0을 Swagger2.0으로 불렀습니다.
이후 Linux Foundation으로 Swagger가 기증되면서 OpenAPI Specification으로 이름이 변경되었습니다.
OpenAPI는 Restful API 디자인에 대한 정의
Swagger는 OpenAPI 정의에 대한 구현체입니다. 예를 들어 Swagger UI를 통하여 OpenAPI spec 문서를 디플로이하고 브라우저에 예쁘게 표시할 수 있습니다.
OpenAPI와 Redoc
Swagger-UI와 비슷하게 OpenAPI Spec 파일을 읽어 문서로 만들어주는 역할을 수행하며 설치와 사용이 아주 간편한 장점이 있지만 Swagger-UI와 달리 브라우저에서 API TEST 기능을 해볼 수 없는 단점이 있습니다.
OpenAPI와 RestDocs
RestDocs 또한 문서를 만드는 방법론입니다.
RestDocs는 테스트 기반의 방법론을 취하며 테스트가 성공해야 문서가 만들어지게 됩니다.
RestDocs와 OpenAPI는 다르게 봐야하지만 RestDocs를 기반으로 만들어지는 문서 설정을 기반으로 OpenAPI Spec 파일을 생성하고 Swagger UI를 띄워서 OpenAPI Spec 파일을 읽는 형식으로 RestDocs를 활용하여 Swagger의 가독성 있는 UI와 test를 통해 검증하는 RestDocs를 둘 다 활용할 수 있습니다.
RestDocs -> OpenAPI Spec -> Swagger-UI
하나의 고민
OpenApi Spec을 활용하여 Swagger로 문서를 만들면서 테스트를 수행하는것처럼 코드를 검증할 순 없을까?
즉, Swagger에서 발생하는 문서와 실제코드의 괴리를 해결할 순 없을까?
OpenAPI 문서를 통하여 코드를 Generate하게 되면 API의 interface와 Response, Request DTO들을 생성해 줍니다.
해당 코드를 사용하면 항상 문서와 코드가 일치되게 관리할 수 있을 것 같습니다.
OpenAPI 문서로 코드 만들기
build.gradle.kts 의존성 추가 및 설정
plugins { //OpenAPI 3.0 문서를 기반으로 코드 생성 id("org.openapi.generator") version "6.4.0" } //Gradle에서 sourceSets 블록을 사용하면 다양한 소스 집합에 대한 소스 디렉터리를 정의할 수 있습니다 sourceSets { main { //이는 Gradle이 해당 디렉터리에 있는 Kotlin 소스 코드를 컴파일한다는 의미입니다. java.srcDir("$buildDir/generated/src/main/kotlin") //Gradle이 생성된 JAR 또는 WAR 파일에 해당 디렉터리에 위치한 리소스 파일을 포함한다는 의미입니다. resources.srcDir("${openApiGenerate.outputDir.get()}/src/main/resources") } } tasks { bootJar { //출력 JAR 파일의 이름이 "boot.jar"가 된다는 것을 의미합니다. archiveFileName.set("boot.jar") } springBoot { //"com.demo.ApiApplicationKt"로 설정되고 있으며, 이는 이 클래스가 Spring Boot 애플리케이션의 진입점으로 사용됨을 의미합니다. mainClass.set("com.demo.ApiApplicationKt") } compileKotlin { //Kotlin 소스 코드를 컴파일하기 전에 이 두 작업이 실행되도록 합니다. dependsOn("openApiGenerate", "openApiJsonGenerator") } } val apiSpecsRoot = "$projectDir/src/api-specs" val specPath = "$apiSpecsRoot/specs/openapi.yaml" openApiGenerate { generatorName.set("kotlin-spring") //kotlin-spring 기반 코드 생성 inputSpec.set(specPath) //OpenApi 3.0문서의 위치 outputDir.set("$buildDir/generated") //문서를 기반으로 생성될 코드의 위치 configFile.set("$apiSpecsRoot/specs/config.json") } tasks.create<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("openApiJsonGenerator") { dependsOn("openApiGenerate") generatorName.set("openapi") //kotlin-spring 기반 코드 생성 inputSpec.set(specPath) //OpenApi 3.0문서의 위치 outputDir.set("$buildDir/resources/main/docs") //문서를 기반으로 생성될 코드의 위치 doLast { copy { from("$apiSpecsRoot/docs") into("$buildDir/resources/main/docs") } } }
OpenAPI 문서로 코드를 만들어내기 위해서 openapi.generator plugin을 추가합니다.
sourceSets, task들을 지정하여 문서를 기반으로 코드를 생성하고 api doc을 만들어냅니다.
api-specs 디렉터리 만들기
outputDir.set("$rootDir/api-external-coupon/api-specs/")
지정된 위치에 디렉터리를 만듭니다.
이후 config.json, openapi.yaml, SignUpRequest.yaml, docs 등 파일을 추가할 예정입니다.
{module-name}/src/api-specs/specs/config.json에 설정 정의
{ "library": "spring-boot", "documentationProvider": "none", "interfaceOnly" : true, "basePackage" : "com.demo", "modelPackage": "com.demo.api.model", "apiPackage": "com.demo.api.controller", "useTags" : true, "skipDefaultInterface" : true, "dateLibrary": "java11", "useBeanValidation" : true, "serializableModel" : true, "implicitHeaders" : false, "swaggerDocketConfig": false, "hideGenerationTimestamp": false, "useSpringfox": false, "reactive": false, "openApiNullable" : false, "enumPropertyNaming": "original" }
basePackage, modelPackage, apiPackage를 자신에 맞는 패키지명으로 수정해주어야 합니다.
model에는 request, response 등이 생성되며, api에는 controller interface가 생성됩니다.
{module-name}/src/api-specs/specs/openapi.yaml에 Spec 정의
openapi: 3.0.0 info: title: Member API version: 1.0.0 servers: - url: http://localhost:8080 tags: - name: signUp description: 회원가입 paths: /member: post: tags: - signUp summary: Create a new member operationId: SignUpRequest requestBody: required: true content: application/json: schema: $ref: './requests/SignUpRequest.yaml' responses: '201': description: Successfully created a new member '400': description: Invalid input data '409': description: Member with provided memberId already exists
해당 문서는 OpenAPI 3.0.0 스펙을 사용하여 작성된 Member API의 스펙 문서입니다.
문서의 첫 부분에서는 API의 제목과 버전 정보, 그리고 서버 정보를 정의합니다.
여기서는 "Member API"의 버전 1.0.0을 사용하며, 서버는 "http://localhost:8080"입니다.
paths 항목에서 API의 엔드포인트와 HTTP 메서드, 요청/응답 바디 스키마, 그리고 각각의 응답 코드와 그에 대한 응답 바디 스키마를 정의합니다.
이 예시에서는 /member 엔드포인트에 대한 POST 메서드를 정의하며, requestBody에는 SignUpRequest.yaml에 정의된 스키마가 사용됩니다.
response에서는 세 가지 HTTP 응답 코드인 201, 400, 409에 대해 각각의 설명이 적혀있습니다.
{module-name}/src/api-specs/requests/SignUpRequest.yaml
type: object properties: memberId: type: string description: Unique identifier for the member password: type: string description: Password for the member's account nickName: type: string description: Nickname for the member fullName: type: string description: Full name of the member required: - memberId - password - nickName - fullName
회원가입에 대한 object의 프로퍼티들을 정의합니다.
4개의 프로퍼티가 모두 필수이며 string 타입으로 정의되어 있습니다.
문서 생성하기
위의 준비를 통해 코드는 성공적으로 만들어집니다.
doLast { copy { from("$apiSpecsRoot/docs") into("$buildDir/resources/main/docs") } }
하지만 아직 마지막에 docs를 copy 하여 resources/main/docs로 붙여 넣을 준비가 안되었습니다.
docs에 redoc, swagger 등의 설정을 추가해야 합니다.
해당 자료는 git repo를 참조해 주시면 좋을 것 같습니다. (api-external-coupon/src/api-specs/docs) 복사&붙여넣기
https://github.com/inara-coupon-study/coupon-system
만들어진 docs를 index.html에 접근 가능하도록 application.yml 설정을 추가합니다.
# api docs index.html으로 접근 가능하도록 설정 spring: web: resources: static-locations: classpath:/docs
주의!
org.gradle.unsafe.configuration-cache=true //Error 발생 cannot serialize Gradle script object references as these are not supported with the configuration cache.
gradle.properties에 위와 같은 옵션이 정의되어 있으면 build시 error가 발생하기 때문에 제거해주어야 합니다.
./gradlew clean generateApi ./gradlew clean build -x test
둘 중 하나의 명령어를 사용하면 문서를 기반으로 kotlin 코드가 생성됩니다.
build/generated/src/.../controller, build/generated/src/.../api에 다음과 같은 코드가 생성되었습니다.
@Validated @RequestMapping("\${api.base-path:}") interface SignUpApi { @RequestMapping( method = [RequestMethod.POST], value = ["/member"], consumes = ["application/json"] ) fun signUpRequest( @Valid @RequestBody signUpRequest: SignUpRequest): ResponseEntity<Unit> { return ResponseEntity(HttpStatus.NOT_IMPLEMENTED) } }
/** * * @param memberId Unique identifier for the member * @param password Password for the member's account * @param nickName Nickname for the member * @param fullName Full name of the member */ data class SignUpRequest( @get:JsonProperty("memberId", required = true) val memberId: kotlin.String, @get:JsonProperty("password", required = true) val password: kotlin.String, @get:JsonProperty("nickName", required = true) val nickName: kotlin.String, @get:JsonProperty("fullName", required = true) val fullName: kotlin.String ) { }
build/resources/main/docs에 swagger redoc이 복사되었습니다.
Build 디렉터리가 보이지 않아요
만약 build 디렉터리가 보이지 않는다면 Project 왼쪽 톱니바퀴 -> Tree appearance -> Show Excluded Files를 선택해 주세요
만들어진 코드로 Controller 구성하기
@RestController class SignUpMockController: SignUpApi { override fun signUpRequest(@Valid @RequestBody signUpRequest: SignUpRequest): ResponseEntity<Unit> { return ResponseEntity(HttpStatus.CREATED) } }
SignUpApi를 상속받아 구현할 수 있습니다.
만들어진 API Docs 확인해 보기
localhost:8080/index.html으로 접속 후 Swagger로 이동한 예시입니다.
SignUpRequest에 대한 명세와 post /members에 대한 자원이 명시되어 있습니다.
참고자료
https://spec.openapis.org/oas/v3.1.0
https://gruuuuu.github.io/programming/openapi/
https://www.lesstif.com/software-engineering/openapi-swagger-redoc-106857823.html
https://tech.kakaopay.com/post/openapi-documentation/
https://plugins.gradle.org/plugin/org.openapi.generator
'프로젝트 > 선착순 쿠폰 발급 시스템' 카테고리의 다른 글
Kotlin MapStruct 사용하기 (0) 2023.05.21 Spring Data Redis TTL설정하기 (0) 2023.05.20 쿠폰 발급을 위한 Redis Set Document 읽기 (0) 2023.05.06 Embedded Redis 구성하여 테스트 수행하기 (0) 2023.05.04 Event Driven Architecture란? (0) 2023.05.01