OpenAPI 및 SpringBoot로 REST API 작성

지금까지 우리는 새로운 스프링 부트 애플리케이션을 생성하는 방법과 이를 컨테이너화하는 방법을 살펴보았습니다. 그러나 우리의 응용 프로그램에는 기능이 없습니다. 오늘 우리는 Spring 부트로 REST API를 생성하는 방법을 볼 것입니다. 우리는 스키마 우선 접근 방식을 취하고 스키마를 사용하여 REST API 스텁을 생성할 것입니다. 이 문서에서는 OpenAPI 사양이 어떻게 생겼는지, 스키마를 사용하여 REST API 스텁을 생성하는 방법을 보여줍니다.

OpenAPI 사양



API는 애플리케이션과 애플리케이션 소비자 간의 계약입니다. 이러한 소비자는 기계일 수도 있고 사람일 수도 있습니다. OpenAPI는 사람과 기계가 읽을 수 있는 형식으로 API 계약을 작성하기 위한 사양입니다. API를 설명할 수 있는 방법을 표준화합니다. 전체 사양은 여기https://spec.openapis.org/oas/v3.1.0에서 찾을 수 있습니다.

첫 번째 API 사양


inventory-service라고 부르는 새 서비스를 만들어 보겠습니다. 이제 우리는 새로운 스프링 부트 애플리케이션을 생성하는 방법을 알고 있습니다. src/resources/spec/inventory-api.yml에 yml openAPI 사양 파일을 추가합니다. 최소 API는 다음과 같습니다.

openapi: "3.0.3"
info:
  title: inventory-api
  version: 1.0.0
paths:
  /products:
    get:
      description: Get All Products
      operationId: getAllProducts
      responses:
        '200':
          description: All products are returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListOfProducts'
        '404':
          description: No Product returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    post:
      description: Add A Product to inventory
      operationId: addProduct
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Product'
      responses:
        '201':
          description: Product added successfully
          headers:
            location:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /products/{id}:
    get:
      description: Get A Product By ID
      operationId: getProductById
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      responses:
        '200':
          description: Get a product by id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          description: No Product returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      description: Update A Product
      operationId: updateProduct
      parameters:
        - in: path
          name: id
          schema:
            type: string
            format: uuid
          required: true
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Product'
      responses:
        '200':
          description: Created product is  returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Product Does not Exist
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    ListOfProducts:
      type: array
      items:
        $ref: '#/components/schemas/Product'

    Product:
      type: object
      properties:
        id:
          type: string
          format: UUID
    Error:
      type: object
      properties:
        message:
          type: string


아주 최소한의 API입니다. paths 섹션에서 API에 대한 설명을 볼 수 있습니다. 각 API 끝점에는 선택적 요청 본문과 응답 본문이 있습니다. 또한 일부 사용자 정의 헤더가 필요한지 여부, 경로 매개변수, 쿼리 매개변수 등을 정의할 수 있습니다.components 섹션에서 모델을 정의하고 API에서 참조합니다.

OpenAPI 사양에 대해 더 깊이 들어가지는 않겠지만 사양이 매우 방대하기 때문에 특정 사용 사례에 대한 사양을 항상 참조할 수 있습니다.

Spring용 REST API 생성



이제 OpenAPI 사양이 있으므로 사양에서 코드를 생성하는 데 사용할 수 있는 플러그인과 도구가 있습니다. openapi-generator https://openapi-generator.tech/docs/installation을 사용하여 REST API를 생성할 수 있습니다. cli를 사용하여 REST API를 생성할 수 있습니다.

소스를 생성하는 데 사용할 maven 플러그인https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-maven-plugin도 있습니다.

maven 플러그인은 openapi-generator를 사용하여 소스 코드를 생성합니다.

maven-plugin을 사용하기 위해 다음과 같이 빌드 섹션에 추가합니다.

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>5.4.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/spec/inventory-api.yml</inputSpec>
                <generatorName>spring</generatorName>
                <generateSupportingFiles>false</generateSupportingFiles>
                <configOptions>
                    <basePackage>com.sab.inventory</basePackage>
                    <sourceFolder>src/java/main</sourceFolder>
                    <interfaceOnly>true</interfaceOnly>
                    <modelPackage>com.sab.inventory.dto</modelPackage>
                    <apiPackage>com.sab.inventory.api</apiPackage>
                    <skipDefaultInterface>true</skipDefaultInterface>
                    <openApiNullable>false</openApiNullable>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>



플러그인과 실제 openapi-generator 모두 https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-maven-pluginhttps://openapi-generator.tech/docs/generators/spring 에서 확인할 수 있는 많은 구성 옵션이 있습니다.

위의 예에서는 최소 구성을 사용했습니다. 아래에서 설명하겠습니다.

* `inputSpec` - This is the path to the OpenAPI spec file.
* `generatorName` - ooenapi-generator can produce source code multiple language and framework. Because we want to generate for Spring I chose spring as the generator name.
* `generateSupportingFiles` - When I generated first I saw some unecessary supporting files were generated as I do not need those I turned it to false.
* `configOptions` - Properties that maps directly to the generator options.
   ** `basePackage` - package name for your generated sources
   ** `sourceFolder` - folder where your generated sources will be placed
   **  `interfaceOnly` - We can either generate only REST API interface or we can generate some default code. I make it true as I want to generate only REST API interface.
   ** `modelPackage` - package name for your generated model/dto classes.
   ** `apiPackage` - package name for your generated API classes.
   **   `skipDefaultInterface` - We can skip the genration of default methods in the interface.
   ** `openApiNullable` - With the value true it will generate an import of `org.openapitools.jackson.nullable.JsonNullable` however I didn't need it so I make it false.

그렇다면 위 코드의 출력은 무엇일까요? 다음은 REST API 스텁의 모습입니다.

/**
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (5.4.0).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */
package com.sab.inventory.api;

import com.sab.inventory.dto.Error;
import com.sab.inventory.dto.Product;
import java.util.UUID;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Generated;

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-04-23T22:00:49.464591+02:00[Europe/Berlin]")
@Validated
@Tag(name = "products", description = "the products API")
public interface ProductsApi {

    /**
     * POST /products
     * Add A Product to inventory
     *
     * @param product  (optional)
     * @return All products are returned (status code 201)
     *         or No Product returned (status code 400)
     */
    @Operation(
        operationId = "addProduct",
        responses = {
            @ApiResponse(responseCode = "201", description = "All products are returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Product.class))),
            @ApiResponse(responseCode = "400", description = "No Product returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Error.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.POST,
        value = "/products",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    ResponseEntity<Product> addProduct(
        @Parameter(name = "Product", description = "", schema = @Schema(description = "")) @Valid @RequestBody(required = false) Product product
    );


    /**
     * GET /products
     * Get All Products
     *
     * @return All products are returned (status code 200)
     *         or No Product returned (status code 404)
     */
    @Operation(
        operationId = "getAllProducts",
        responses = {
            @ApiResponse(responseCode = "200", description = "All products are returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Product.class))),
            @ApiResponse(responseCode = "404", description = "No Product returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Error.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/products",
        produces = { "application/json" }
    )
    ResponseEntity<List<Product>> getAllProducts(

    );


    /**
     * GET /products/{id}
     * Get A Product By ID
     *
     * @param id  (required)
     * @return All products are returned (status code 200)
     *         or No Product returned (status code 404)
     */
    @Operation(
        operationId = "getProductById",
        responses = {
            @ApiResponse(responseCode = "200", description = "All products are returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Product.class))),
            @ApiResponse(responseCode = "404", description = "No Product returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Error.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/products/{id}",
        produces = { "application/json" }
    )
    ResponseEntity<Product> getProductById(
        @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") UUID id
    );


    /**
     * PUT /products/{id}
     * Update A Product
     *
     * @param id  (required)
     * @param product  (optional)
     * @return Created product is  returned (status code 200)
     *         or Error (status code 400)
     *         or Product Does not Exist (status code 404)
     */
    @Operation(
        operationId = "updateProduct",
        responses = {
            @ApiResponse(responseCode = "200", description = "Created product is  returned", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Product.class))),
            @ApiResponse(responseCode = "400", description = "Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Error.class))),
            @ApiResponse(responseCode = "404", description = "Product Does not Exist", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  Error.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.PUT,
        value = "/products/{id}",
        produces = { "application/json" },
        consumes = { "application/json" }
    )
    ResponseEntity<Product> updateProduct(
        @Parameter(name = "id", description = "", required = true, schema = @Schema(description = "")) @PathVariable("id") UUID id,
        @Parameter(name = "Product", description = "", schema = @Schema(description = "")) @Valid @RequestBody(required = false) Product product
    );

}


이제 API 인터페이스가 있으므로 컨트롤러를 만들고 메서드를 구현할 수 있습니다.

이번 포스팅은 여기까지 하고, 실제로 테스트를 통해 API를 구현할 예정이니 다음 포스팅을 기대해주세요.

좋은 웹페이지 즐겨찾기