상세 컨텐츠

본문 제목

소라브 샤르마, 『스프링 6와 스프링 부트 3로 배우는 모던 API 개발』 3장

Development Study/개발 관련 도서

by yooputer 2025. 1. 13. 09:07

본문

API 명세 및 구현

개요

  • 1장과 2장의 지식을 사용해 REST API 구현. 구현을 위해 설계 우선 접근 방식(desing-first approach) 선택
  • OpenAPI 명세를 사용해 API 먼저 설계, 나중에 구현
  • OpenAPI 코드 생성기를 사용하여 모델 및 API 자바 인터페이스에 대한 코드 생성 방법을 배운다. 

OAS로 API 설계

  • 설계 없이 API 코딩을 바로 시작하다보면 잦은 수정, API 관리의 어려움, 개발 외 부서가 API를 검토할 때 생기는 어려움 등 여러 문제가 생기기 때문에 설계 우선 접근 방식을 사용해야 한다. 
  • OAS(OpenAPI Specification)는 REST API의 명세 및 설명을 위해 도입됐다. 
  • YAML 또는 JSON 마크업 언어로 REST API를 작성할 수 있다
  • OAS는 Swagger 명세에 많이 사용하며 알려지게 되었다. 그래서 OAS 지원 도구를 흔히 Swagger 도구라고 부르기도 한다. 
  • 이 장에서는 Swagger Editor, Swwager Codegen, Swagger UI를 사용할 예정이다. 

OAS로 기본 구조 이해

  • OpenAPI 정의 구조는 다음과 같다.
    openapi(버전),
    info, externalDocs, servers, tags, paths, components
  • 처음 세 부분은 API의 메타데이터를 정의하는데 사용한다 
  • API 정의는 단일 파일에 작성하거나 여러 파일로 나누어 작성할 수 있으며 OAS는 둘 다 지원한다. 

OAS의 메타데이터 절

  • openapi : OpenAPI 버전
  • info : API에 대한 메타데이터. title과 version은 필수 필드다. 
    • title : API 제목
    • description : API 설명. 마크다운 사용 가능
    • termOfService : 서비스 약관으로 연결되는 URL
    • contact : API 제공자의 연락처 정보
    • license : 라이센스 정보
    • version : API 버전
  • externalDocs : API의 확장 문서 정보. 선택적 필드
openapi: 3.0.3
info:
  title: Sample Ecommerce App
  description: >
    'This is a ***sample ecommerce app API***.  You can find out more about Swagger at [swagger.io](http://swagger.io).
    Description supports markdown markup. For example, you can use the `inline code` using back ticks.'
  termsOfService: https://github.com/PacktPublishing/Modern-API-Development-with-Spring-6-and-Spring-Boot-3/blob/main/LICENSE
  contact:
    name: Packt Support
    url: https://www.packt.com
    email: support@packtpub.com
  license:
    name: MIT
    url: https://github.com/PacktPublishing/Modern-API-Development-with-Spring-6-and-Spring-Boot-3/blob/main/LICENSE
  version: 1.0.0
externalDocs:
  description: Any document link you want to generate along with API.
  url: http://swagger.io

OAS의 servers와 tags 절

  • servers : API를 호스팅하는 서버 목록. 선택적 항목. 값을 지정하지 않으면 호스트된 문서 서버의 루트를 가리킨다. 
  • tags : tag와 tag 메타데이터의 컬렉션 포함. Tags는 리소스가 수행하는 오퍼레이션을 그룹화하는데 사용 
    • name : 태그명. 필수값
    • description : 태그 설명
    • externalDocs : 확장 문서 정보
servers:
  - url: https://ecommerce.swagger.io/v2
tags:
  - name: cart
    description: Everything about cart
    externalDocs:
      description: Find out more (extra document link)
      url: http://swagger.io
  - name: order
    description: Operation about orders
  - name: user
    description: Operations about users
  - name: customer
    description: Operations about user's persona customer
  - name: address
    description: Operations about user's address
  - name: payment
    description: Operations about payments
  - name: shipping
    description: Operations about shippings
  - name: product
    description: Operations about products
  - name: card 
    description: card operation

OAS의 컴포넌트 절

  • 컴포넌트 절에서는 API에 사용하는 모델을 정의한다. 
components:
  schemas:
    Cart:
      description: Shopping Cart of the user
      type: object
      properties:
        customerId:
          description: Id of the customer who possesses the cart
          type: string
        items:
          description: Collection of items in cart.
          type: array
          items:
            $ref: '#/components/schemas/Item'

 

OAS 기본 데이터 타입

  • string, number, integer, boolean, object, array

OAS 상세 데이터 타입 정의

  • type에 기본 데이터타입을 정의하고, format에 상세 데이터 타입을 정의한다. 
<필드명>:
	type: number
    format: date-time
기본 데이터 타입 상세 데이터 타입 설명
number float float 타입 실수
number double double 타입 실수
integer int32 int 타입 정수
integer int64 long 타입 정수
string date yyyy-MM-dd 형식의 날짜
string date-time yyyy-MM-ddThh:mm:ss 형식의 날짜
string byte Base62로 인코딩한 값
string binary 이진 데이터, 파일에 사용

참조 객체 표현

  • Cart 모델의 items 필드는 사용자 정의 Item 타입의 배열이다. 
    여기서 Item은 또 다른 모델이며 $ref를 사용하여 참조된다. 
  • Item 모델은 components/schema 절의 일부이기도 하다

OAS의 경로 절

  • path절에서는 API의 엔드포인트를 정의한다.
paths:
  /api/v1/carts/{customerId}:
    get:
      tags:
        - cart
      summary: Returns the shopping cart
      description: Returns the shopping cart of given customer
      operationId: getCartByCustomerId
      parameters:
        - name: customerId
          in: path
          description: Customer Identifier
          required: true
          schema:
            type: string
      responses:
        200:
          description: successful operation
          content:
            application/xml:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Cart'
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Cart'
        404:
          description: Given customer ID doesn't exist
          content: {}

메소드별 필드

tags API를 그룹화할 때 사용
summary 요약
description 설명. 마크다운 사용 가능
operationId 오퍼레이션 명. Swagger Codegen에서 사용한 API 인ㅇ터페이스의 메소드 명으로 사용됨
parameters 파라미터에 대한 정보 명시
responses API가 요청에 대해 응답할 수 있는 응답 타입 정의
requestBody 요청 페이로드 객체 정의

OAS를 스프링 코드로 변환

dependency 추가

  • build.gradle
swaggerCodegen 'org.openapitools:openapi-generator-cli:6.2.1'
compileOnly 'io.swagger:swagger-annotations:1.6.4'
compileOnly 'org.springframework.boot:spring-boot-starter-validation'
compileOnly 'org.openapitools:jackson-databind-nullable:0.2.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
// required for schema in swagger generated code
implementation 'io.springfox:springfox-oas:3.0.0'

Swagger 플러그인 추가

  • build.gradle
plugins {
    ...
    id 'org.hidetake.swagger.generator' version '2.19.2'
}

OpenAPI config 정의

  • config.json
{
  "library": "spring-boot",
  "dateLibrary": "java8",
  "hideGenerationTimestamp": true,
  "modelPackage": "com.packt.modern.api.model",
  "apiPackage": "com.packt.modern.api",
  "invokerPackage": "com.packt.modern.api",
  "serializableModel": true,
  "useTags": true,
  "useGzipFeature" : true,
  "hateoas": true,
  "unhandledException": true,
  "useSpringBoot3": true,
  "useSwaggerUI": true,
  "importMappings": {
    "ResourceSupport":"org.springframework.hateoas.RepresentationModel",
    "Link": "org.springframework.hateoas.Link"
  }
}

 

OpenAPI 생성기 ignore 파일 정의

  • .openapi-generator-ignore
**/*Controller.java

openapi.yaml 파일 정의

swaggerSources 태스크 정의

  • build.gradle
swaggerSources {
    def typeMappings = 'URI=URI'
    def importMappings = 'URI=java.net.URI'
    eStore {
        def apiYaml = "${rootDir}/src/main/resources/api/openapi.yaml"
        def configJson = "${rootDir}/src/main/resources/api/config.json"
        inputFile = file(apiYaml)
        def ignoreFile = file("${rootDir}/src/main/resources/api/.openapi-generator-ignore")
        code {
            language = 'spring'
            configFile = file(configJson)
            rawOptions = ['--ignore-file-override', ignoreFile, '--type-mappings',
                          typeMappings, '--import-mappings', importMappings] as List<String>
            components = [models: true, apis: true, supportingFiles: 'ApiUtil.java']
            dependsOn validation
        }
    }
}

 

compileJava 작업 의존성에 swaggerSources 추가

  • build.gradle
compileJava.dependsOn swaggerSources.eStore.code

processResources {
  dependsOn(generateSwaggerCode)
}

생성된 소스 코드를 Gradle sourceSets에 추가

sourceSets.main.java.srcDir "${swaggerSources.eStore.code.outputDir}/src/main/java"
sourceSets.main.resources.srcDir "${swaggerSources.eStore.code.outputDir}/src/main/resources"

코드 생성, 컴파일 및 빌드

  • gradlew clean build

OAS 코드 인터페이스 구현

  • Swagger Codegen은 각 태그에 대한 API 인터페이스를 생성하는 기능이다. 
  • cart 태그가 있는 모든 API의 경우 CartApi.java로 묶인다. 

전역 예외 처리기 추가

Error 클래스 정의

@Getter
@Setter
public class Error {
  private String errorCode;
  private String message;
  private Integer status;
  private String url = "Not available";
  private String reqMethod = "Not available";
}

ErrorCode 이넘 정의

@Getter
@AllArgsConstructor
public enum ErrorCode {
  GENERIC_ERROR("PACKT-0001", "The system is unable to complete the request. Contact system support."),
  HTTP_MEDIATYPE_NOT_SUPPORTED("PACKT-0002", "Requested media type is not supported. Please use application/json or application/xml as 'Content-Type' header value"),

  private String errCode;
  private String errMsgKey;
}

ErrorUtils 클래스 구현

@NoArgsConstructor
public class ErrorUtils {
  public static Error createError(final String errMsgKey, final String errorCode,
      final Integer httpStatusCode) {
    Error error = new Error();
    error.setMessage(errMsgKey);
    error.setErrorCode(errorCode);
    error.setStatus(httpStatusCode);
    return error;
  }
}

RestApiErrorHandler 클래스 구현

@RequiredArgsConstructor
@ControllerAdvice
public class RestApiErrorHandler {
  private final MessageSource messageSource;

  @ExceptionHandler(Exception.class)
  public ResponseEntity<Error> handleException(HttpServletRequest request, Exception ex,
                                               Locale locale) {
    ex.printStackTrace(); // TODO: Should be kept only for development
    Error error = ErrorUtils
        .createError(ErrorCode.GENERIC_ERROR.getErrMsgKey(), ErrorCode.GENERIC_ERROR.getErrCode(),
            HttpStatus.INTERNAL_SERVER_ERROR.value()).setUrl(request.getRequestURL().toString())
        .setReqMethod(request.getMethod());
    return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
  }

  @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
  public ResponseEntity<Error> handleHttpMediaTypeNotSupportedException(HttpServletRequest request,
      HttpMediaTypeNotSupportedException ex,
      Locale locale) {
    ex.printStackTrace(); // TODO: Should be kept only for development
    Error error = ErrorUtils
        .createError(ErrorCode.HTTP_MEDIATYPE_NOT_SUPPORTED.getErrMsgKey(),
            ErrorCode.HTTP_MEDIATYPE_NOT_SUPPORTED.getErrCode(),
            HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()).setUrl(request.getRequestURL().toString())
        .setReqMethod(request.getMethod());
    log.info("HttpMediaTypeNotSupportedException :: request.getMethod(): " + request.getMethod());
    return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
  }
}

API 구현 테스트

curl --request GET 'http://localhost:8080/api/v1/carts/1' \
--header 'Content-Type: application/json'\
--header 'Accept: application/json'\
--data-raw '{"id":"1", "quantity":1, "unitPrice":2.5}'

 

관련글 더보기