본문 바로가기
프로그래밍/기타

Open API Spefication 활용하기 - 4편 ( yaml 파일 기반으로 자동으로 retrofit2 코드 생성하기 )

by 사바라다 2022. 9. 15.

안녕하세요. 오늘은 Open API Specification 활용하기 마지막편입니다. 오늘 알아볼 내용은 3편에서 자동으로 만든 Open API Specification yaml을 기반으로 자동으로 retrofit2 코드를 만들어보도록 하겠습니다.

환경

Client의 환경은 이전 프로젝트와 동일하게 Spring Boot, Kotlin 환경으로 진행하도록 하겠습니다.

  • kotlin 1.6.21
  • Spring Boot 2.7.3
  • gradle kotlin dsl 7.5

yaml 파일

자동으로 만들어낼 client Open API Specification 파일은 아래와 같습니다. 아래 파일은 이전 시간 3편에서 자동으로 만들어낸 yaml 파일과 동일한 파일입니다. 3개의 API Operation을 가지고 2개의 DTO Model을 가지고 있는 파일입니다.

더보기
openapi: 3.0.1
info:
  title: OpenAPI definition
  version: v0
servers:
- url: http://localhost:8080
  description: Generated server url
paths:
  /api/open/:
    get:
      tags:
      - open-api-controller
      operationId: findResponses
      responses:
        "200":
          description: OK
          content:
            '*/*':
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OpenApiResponse'
    put:
      tags:
      - open-api-controller
      operationId: createBook
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OpenApiRequest'
        required: true
      responses:
        "200":
          description: OK
          content:
            '*/*':
              schema:
                $ref: '#/components/schemas/OpenApiResponse'
  /api/open/{id}:
    get:
      tags:
      - open-api-controller
      operationId: findById
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: integer
          format: int64
      responses:
        "200":
          description: OK
          content:
            '*/*':
              schema:
                $ref: '#/components/schemas/OpenApiResponse'
components:
  schemas:
    OpenApiRequest:
      type: object
      properties:
        string:
          type: string
        number:
          type: integer
          format: int32
        number2:
          type: integer
          format: int64
        number3:
          type: number
          format: double
        time:
          type: string
          format: date-time
    OpenApiResponse:
      type: object
      properties:
        string:
          type: string
        number:
          type: integer
          format: int32
        number2:
          type: integer
          format: int64
        number3:
          type: number
          format: double
        time:
          type: string
          format: date-time

build.gradle.kts

yaml 코드를 openapi generator를 통해서 자동으로 생성합니다. 여기서는 gradle kotlin dsl을 통해서 진행해보도록 하겠습니다. maven, cli 등 다양한 방법을 제공하고 있으니 편한 도구를 선택하시면 좋을것 같습니다.

plugins {
    id("org.openapi.generator") version "6.0.1"
}

gradle 에서 client code generator를 하기 위해서는 org.openapi.generator 플러그인을 이용할 수 있습니다.

dependencies {
    // for retrofit2 client
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
    implementation("com.squareup.retrofit2:converter-scalars:2.9.0")

    implementation("com.squareup.moshi:moshi-kotlin:1.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")
}

retrofit2 코드를 자동으로 만들어내는것이기 때문에 이와 관련된 의존성을 위처럼 함께 가져옵니다.

openApiGenerate {
    generatorName.set("kotlin")
    inputSpec.set("$rootDir/docs/swagger.yaml")
    outputDir.set("$buildDir/classes/generated")
    apiPackage.set("org.openapi.example.api")
    invokerPackage.set("org.openapi.example.invoker")
    modelPackage.set("org.openapi.example.model")
    configOptions.set(mapOf(
        "dateLibrary" to "java8"
    ))
    additionalProperties.set(mapOf(
        "library" to "jvm-retrofit2"
    ))
}

그리고 openApiGenerate task의 옵션을 정의하여줍니다. 각 정의되는 내용은 open api generator 문서를 참고하시면 더 많은 정보를 얻어가실 수 있습니다. 이렇게 task를 정의한 후 task를 실행해보도록 하겠습니다. task 실행은 gradle openApiGenerate를 통해 할 수 있습니다.

generated Class

gradle에서 task가 실행이 되고 아래처럼 kotlin 기반의 retfofit2 client 코드가 자동으로 생성되었습니다.

interface OpenApiControllerApi {
    /**
     * 
     * 
     * Responses:
     *  - 200: OK
     *
     * @param openApiRequest 
     * @return [Call]<[OpenApiResponse]>
     */
    @PUT("api/open/")
    fun createBook(@Body openApiRequest: OpenApiRequest): Call<OpenApiResponse>

    /**
     * 
     * 
     * Responses:
     *  - 200: OK
     *
     * @param id 
     * @return [Call]<[OpenApiResponse]>
     */
    @GET("api/open/{id}")
    fun findById(@Path("id") id: kotlin.Long): Call<OpenApiResponse>

    /**
     * 
     * 
     * Responses:
     *  - 200: OK
     *
     * @return [Call]<[kotlin.collections.List<OpenApiResponse>]>
     */
    @GET("api/open/")
    fun findResponses(): Call<kotlin.collections.List<OpenApiResponse>>

}
/**
 * 
 *
 * @param string 
 * @param number 
 * @param number2 
 * @param number3 
 * @param time 
 */

data class OpenApiRequest (

    @Json(name = "string")
    val string: kotlin.String? = null,

    @Json(name = "number")
    val number: kotlin.Int? = null,

    @Json(name = "number2")
    val number2: kotlin.Long? = null,

    @Json(name = "number3")
    val number3: kotlin.Double? = null,

    @Json(name = "time")
    val time: java.time.OffsetDateTime? = null

)
/**
 * 
 *
 * @param string 
 * @param number 
 * @param number2 
 * @param number3 
 * @param time 
 */

data class OpenApiResponse (

    @Json(name = "string")
    val string: kotlin.String? = null,

    @Json(name = "number")
    val number: kotlin.Int? = null,

    @Json(name = "number2")
    val number2: kotlin.Long? = null,

    @Json(name = "number3")
    val number3: kotlin.Double? = null,

    @Json(name = "time")
    val time: java.time.OffsetDateTime? = null

)

TEST

그러면 실제로 Server 코드와 Generate 된 Client 코드가 유기적으로 잘 통신하는지 확인하기 위해서 테스트를 해보도록 하겠습니다. Spring Boot 환경에서 사용할 것이기 때문에 아래처럼 retrofit2 사용을 위해서 @Bean을 하나 만들어줍니다. ApiClient는 코드 제너레이트 될 때 함께 생성되는 OkHttpClient입니다. 이는 본인이 직접 구성한 것을 사용하셔도 크게 상관 없습니다.

@Bean
fun openApiClientApi(): OpenApiControllerApi {
    return ApiClient("http://localhost:8081")
        .createService(OpenApiControllerApi::class.java)
}

그리고 Service 코드를 만든 후 호출하여 테스트 해보았습니다. 정상적으로 로깅되고 잘 응답값을 가져오는 것을 확인하였습니다.

@Service
class OpenApiClientService(
    private val openApiControllerApi: OpenApiControllerApi
) {

    fun aa() {
        val findResponses = openApiControllerApi.findResponses().execute()

        println("findResponses = ${findResponses.body()}") // findResponses = [OpenApiResponse(string=string, number=1, number2=2, number3=3.3, time=2022-09-15T12:23:29.801149Z), OpenApiResponse(string=string, number=2, number2=4, number3=6.6, time=2022-09-15T12:23:29.801160Z)]
    }
}

마무리

오늘까지의 내용을 토대로하면 우리는 서버 Controller 코드를 수동으로 작성하는 것 만으로도 Open API Specification 기반의 yaml 파일을 만들어 낼 수 있고, Client에서 Server를 호출할 수 있는 코드까지 자동 생성할 수 있다는 것을 보았습니다. 이를 잘 활용하면 API 스펙을 server와 client가 잘못 작성하는 일은 줄어들 것입니다.

감사합니다.

참조

[1] https://github.com/OpenAPITools/openapi-generator

[2] https://openapi-generator.tech/docs/generators/kotlin/

댓글