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 추가
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 플러그인 추가
plugins {
...
id 'org.hidetake.swagger.generator' version '2.19.2'
}
OpenAPI config 정의
{
"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 태스크 정의
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 추가
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"
코드 생성, 컴파일 및 빌드
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}'