OAS(Open API Specification)는 REST API 명세 및 설명을 위한 표준으로, API 설계의 설계 우선 접근 방식(design-first)을 지원한다.
YAML(YAML Ain't Markuup Language) 또는 JSON(JavaScript Object Notation ) 형식으로 REST API를 정의하며, OAS 3.0이 많이 사용된다.
API 설계를 코드 작성 전에 명확히 정의하여 수정, 관리, 확장성 문제를 줄인다.
설계가 명확하면 개발 중 혼란을 줄이고 협업과 문서화를 쉽게 만든다.
OAS 기반 API 설계를 위한 대표적인 도구들은 다음과 같다.
Swagger Editor
웹 기반 도구로, YAML 형식으로 API 설계를 작성하고 즉시 미리보기가 가능하다.
OAS 3.0 설정을 지원하며, 초보자에게 적합하다.
Swagger Codegen
설계한 API 명세를 기반으로 코드(JAVA, Python 등)를 자동 생성한다.
개발 시간을 줄이고, 표준화된 API 구현을 돕는다.
Swagger UI
작성한 OAS 문서를 시각화하여 API 인터페이스를 확인하거나 테스트 가능.
클라이언트와 협업 시 API 이해를 돕는 데 유용하다.
OpenAPI 명세는 다음과 같은 주요 키워드와 계층 구조로 구성된다.
openapi
OpenAPI 명세의 버전을 정의 . (예: 3.0.0)
info
API의 메타데이터를 포함하며, API의 제목, 설명, 버전 등의 정보를 정의한다.
externalDocs
API에 대한 추가 문서 링크를 제공한다.
위 세가지 항목은 API의 메타데이터를 정의하는 데 사용된다.
servers
API가 배포된 서버의 정보를 정의한다. (예: https://api.example.com)
tags
API를 주제별로 구분하기 위해 사용. 태그를 통해 관련된 API를 그룹화한다.
paths:
API의 경로(URI)와 각 경로에서 지원하는 메서드(예: GET, POST)를 정의한다.
components
공통적으로 사용되는 스키마, 응답, 매개변수, 보안 정의 등을 포함한다.
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
openapi
OpenAPI 명세의 버전을 정의한다.
info
API에 대한 메타데이터를 제공하는 섹션으로, 주로 제목, 설명, 서비스 약관, 연락처, 라이선스 정보를 포함한다. info에는 다음 필드가 포함되며, title과 version만 필수 필드이고 나머지는 선택 필드이다.
title
API의 이름 또는 제목이다.
description
API의 세부 설명이다. Markdown 문법을 지원하므로, 텍스트 서식, 링크, 코드 블록 등을 추가할 수 있다.
termsOfService
API 사용 시 적용되는 서비스 약관의 링크이다.
contact
API와 관련된 지원 팀 또는 담당자의 연락처 정보를 의미한다.
name
담당 팀 또는 사람의 이름.
url
연락처 페이지 URL.
email
이메일 주소.
license
API에 적용된 라이선스 정보이다.
name
라이선스 이름(예: MIT).
url
라이선스의 자세한 내용을 확인할 수 있는 링크이다.
version
API의 현재 버전을 나타낸다.
externalDocs
API와 관련된 추가 외부 문서 링크를 제공.
description
문서의 간단한 설명을 의미한다.
url
문서의 링크이다.
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
servers
API거 배포된 서버 정보를 정의한다.
tags
API의 엔드포인트를 주제별로 그룹화하는 데 사용한다. 각 태그는 특정 API 관련 작업을 설명하며, Swagger UI에서 카테고리로 나타낸다.
name
태그의 이름을 의미한다.
description
태그의 설명을 나타낸다.
externalDocs
태그와 관련된 추가 문서 링크를 제공한다.
description
문서의 설명이다.
url
추가 문서의 URL이다.
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'
components
OpenAPI의 공통 재사용 요소를 정의하는 섹션이다. 스키마, 보안 설정, 응답, 매개변수 등을 포함하며, 여러 경로에서 참조($ref) 가능하다.
schemas
데이터 모델을 정의하는 부분으로, API에서 사용되는 객체와 속성의 구조를 정의한다.
$ref는 재사용 가능한 스키마를 참조하기 위한 경로를 나타낸다.
#/components/schemas/Item은 OpenAPI 명세의 components.schemas 섹션에서 정의된 Item 스키마를 참조한다.
Cart 모델의 items 필드는 배열로, 배열의 각 요소는 Item 스키마를 참조한다.
$ref는 쉽게 말해, "Item은 여기저기서 쓰이니, 정의를 재사용하자"는 뜻으로, 반복 없이 깔끔하게 작성하도록 도와주는 참조 기능이다.
OAS는 다음 6가지 기본 데이터 타입을 지원한다.
type: string
format: date
부동 소수점 숫자를 포함한다.
format: byte
Base64로 인코딩한 값을 포함한다.
format: binary
이진 데이터를 포함한다.(파일에 사용 가능)
type: number
format: float
부동 소수점 숫자를 포함한다.
format: double
배정밀도(double precision)의 부동 소수점 숫자를 포함한다.
type: integer
format: int32
int 타입(부호 있는 32비트 정수)을 포함한다.
format: int64
long 타입(부호 있는 64비트 정수)을 포함한다.
type: boolean
type: object
type: array
cf. components 절에서 requestBodies(요청 페이로드)와 responses을 정의할 수도 있다. 이는 공통 요청 본문과 응답이 있는 경우에 유용하다.
path 절에서는 URI를 구성하고 HTTP 메서드를 작성한다.
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: {}
paths
API 경로(/api/v1/carts/{customerId})와 관련된 엔드포인트 정의를 나타낸다.
tags
해당 엔드포인트의 카테고리를 나타낸다.
summary
API의 간단한 설명을 나타낸다.
description
API의 상세 설명을 나타낸다.
operationId
API 메서드의 고유 식별자를 나타낸다. 쉽게 말해, 해당 API가 호출하는 메서드를 의미한다.
parameters
경로 매개변수를 의미한다.
in: path
해당 매개변수가 경로에 포함된다는 뜻이다. ex. /api/v1/carts/12345
required: true
반드시 제공되어야 한다.
schema
데이터타입은 string이다.
응답 정의(responses)
모든 API 오퍼레이션의 필수 필드이다. 기본 필드로 HTTP 상태코드가 포함된다.
200
description (응답 설명)
성공적으로 쇼핑 카트를 반환했음을 나타낸다.
content
반환 데이터 형식은 XML과 JSON 모두 지원한다.
두 형식의 데이터는 배열(type: array)이며, 각 배열 항목은 Cart 스키마($ref: '#/components/schemas/Cart')를 따른다.
404
OpenAPI 명세 파일(OAS, openapi.yaml)을 기반으로 API 모델 클래스 및 인터페이스 코드를 자동 생성한다.
수동으로 API 코드를 작성하는 대신, Swagger Codegen과 같은 도구를 사용하여 빠르고 일관성 있게 코드 생성할 수 있다.
dependencies {
// OpenAPI Starts
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'
// OpenAPI Ends
}
swaggerCodegen 'org.openapitools:openapi-generator-cli:6.2.1'
OpenAPI Generator CLI를 사용하기 위한 의존성이다.
OpenAPI 명세(openapi.yaml)를 기반으로 코드 생성 도구를 제공한다.
이 라이브러리를 통해 모델 클래스, 컨트롤러 인터페이스 등을 자동으로 생성한다.
사용 버전: 6.2.1.
compileOnly 'io.swagger:swagger-annotations:1.6.4'
Swagger를 위한 어노테이션 지원 라이브러리이다.
코드에 Swagger 어노테이션을 추가해 API 문서를 명확하게 정의할 수 있다.
주로 사용하는 어노테이션
@Api
클래스에 대한 설명을 추가한다.
@ApiOperation
메서드 설명을 추가한다.
@ApiParam
요청 매개변수를 설명한다.
@ApiResponse
응답에 대한 설명이다.
compileOnly 'org.springframework.boot:spring-boot-starter-validation'
Spring Boot Validation 라이브러리이다.
요청 데이터의 유효성 검사를 지원한다.
Javax Bean Validation API(@Valid, @NotNull, @Size 등)와 연동되어 데이터 검증을 수행한다.
compileOnly 'org.openapitools:jackson-databind-nullable:0.2.3'
OpenAPI에서 nullable 속성을 처리하기 위한 Jackson 모듈이다.
OpenAPI 명세에서 nullable: true가 설정된 필드를 처리할 때 사용한다.
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
Jackson에서 XML, JSON 데이터를 읽고 쓰기 위한 데이터 포맷 라이브러리이다.
JSON뿐 아니라 XML 포맷을 직렬화/역직렬화할 때 사용한다.
주로 XML 요청/응답이 필요한 API에서 활용한다.
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
HATEOAS(Hypermedia As The Engine Of Application State) 지원 라이브러리이다.
REST API에 링크 기반 내비게이션을 추가할 수 있다.
OpenAPI에서 HATEOAS를 활용하는 경우 필수이다.
implementation 'io.springfox:springfox-oas:3.0.0'
Springfox OAS는 Spring 프로젝트에서 OpenAPI 명세를 자동으로 생성해주는 도구이다.
컨트롤러 메서드와 엔드포인트를 기반으로 OpenAPI 3.0 문서를 생성한다.
Swagger UI와 연동되어 시각화된 API 문서를 제공한다.
ex.
@EnableOpenApi 어노테이션을 통해 OpenAPI를 활성화한다.
컨트롤러 메서드와 엔드포인트가 OpenAPI 명세로 자동 변환된다.
의존성 기능 swaggerCodegen
OpenAPI 명세를 기반으로 코드 생성 swagger-annotations
Swagger 문서화를 위한 어노테이션 지원 spring-boot-starter-validation
요청 데이터 유효성 검사 지원 jackson-databind-nullable
OpenAPI nullable 필드 처리 jackson-dataformat-xml
JSON 외에 XML 데이터를 처리 spring-boot-starter-hateoas
HATEOAS를 통해 API 링크 지원 springfox-oas
Spring 프로젝트에서 OpenAPI 3.0 명세 자동 생성 및 Swagger UI 연동
Swagger Codegen을 사용하려면 Gradle에 플러그인을 추가한다.
plugins {
id 'org.hidetake.swagger.generator' version '2.19.2'
}
코드 생성을 위한 설정 파일(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,
"useBeanValidation": true,
"hateoas": true,
"unhandledException": true,
"useSpringBoot3": true,
"useSwaggerUI": true,
"importMapping": {
"ResourceSupport": "org.springframework.hateoas.RepresentationModel",
"Link": "org.springframework.hateoas.Link"
}
}
키 설명 값의 의미 library
생성할 코드의 기본 라이브러리를 설정한다. "spring-boot"
: Spring Boot와 호환되는 코드가 생성됨.dateLibrary
날짜/시간 처리를 위한 라이브러리 설정. "java8"
: Java 8의LocalDate
,LocalDateTime
등을 사용.hideGenerationTimestamp
코드 생성 시 타임스탬프 포함 여부. true
: 생성된 코드 주석에 타임스탬프를 포함하지 않음.modelPackage
생성된 모델 클래스가 포함될 패키지 경로. "com.packt.modern.api.model"
: 모델 클래스(User
,Order
등)가 이 경로에 생성됨.apiPackage
생성된 API 인터페이스가 포함될 패키지 경로. "com.packt.modern.api"
: API 인터페이스(UserApi
,OrderApi
)가 이 경로에 생성됨.invokerPackage
Invoker 클래스가 포함될 패키지 경로. "com.packt.modern.api"
: Invoker 클래스(필요 시)가 이 경로에 생성됨.serializableModel
모델 클래스의 직렬화 가능 여부 설정. true
: 모델 클래스가Serializable
을 구현해 파일 저장이나 전송 가능.useTags
OpenAPI 명세의 태그를 기반으로 코드 그룹화. true
: 태그별로 관련 API 메서드가 그룹화됨.useBeanValidation
유효성 검사를 위한 Java Bean Validation 어노테이션 추가. true
:@NotNull
,@Size
등의 유효성 검증 어노테이션이 추가됨.hateoas
하이퍼미디어 링크(HATEOAS) 지원 여부 설정. true
: HATEOAS 관련 클래스(RepresentationModel
,Link
)를 활용한 링크 생성 가능.unhandledException
처리되지 않은 예외를 위한 기본 예외 처리 코드 생성 여부. true
: 기본 예외 응답 로직을 생성.useSpringBoot3
Spring Boot 3.x 호환 코드 생성 여부. true
: Spring Boot 3.x와 호환되는 코드가 생성됨.useSwaggerUI
Swagger UI 활성화 여부. true
: Swagger UI를 통해 API 테스트 및 문서 시각화 가능.importMapping
특정 클래스나 타입을 Spring HATEOAS 클래스로 매핑. "ResourceSupport": "RepresentationModel", "Link": "org.springframework.hateoas.Link"
: 매핑.
cf. importMapping 내부 설정 값은 openAPI Generator CLI가 Spring HATEOAS의 최신 표준을 따르기 위해 설정한 것이다.
.openapi-generator-ignore 파일을 사용하여 생성에서 제외할 파일을 지정할 수 있다.
**/Controller.java
cf. OpenAPI Generator는 표준화된 스펙을 기반으로 모델, 인터페이스, DTO 등을 생성하는데 주로 사용하고, 컨트롤러처럼 프로젝트 고유의 비즈니스 로직이나 동작 방식을 포함해야 하는 코드는 자동 생성에서 제외되는 경우가 많다.
src/main/resources/api에 openapi.yaml 파일을 작성해준다.
build.gradle 파일에서 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
}
}
}
typeMapping
OpenAPI 명세의 타입(URI)을 Java의 특정 타입(URI)으로 매핑한다.
importMappings
typeMappings에서 매핑된 Java 타입(URI)에 대해 정확한 import 경로(java.net.URI)를 명시한다.
cf. 자주 사용되는 타입 매핑
format: uri → java.net.URI
format: date → java.time.LocalDate
format: date-time → java.time.LocalDateTime
type: number, format: double → java.math.BigDecimal
eStore
OpenAPI 명세 파일, 설정 파일, 생성 코드의 구성 등을 정의하는 작업 블록이다.
inputFile
OpenAPI 스펙 파일(openapi.yaml)을 지정합니다.
configFile
설정 파일(config.json)을 지정합니다.
rawOptions
OpenAPI Generator 실행 시 명령줄 옵션을 전달하는 역할을 한다.
--ignore-file-override
OpenAPI Generator가 무시할 파일/디렉토리를 정의하는 .openapi-generator-ignore 파일을 사용하도록 설정한다.
--type-mappings
OpenAPI 명세의 타입을 typeMappings 변수에 정의된 매핑 값으로 매핑한다.
--import-mappings
OpenAPI 명세의 특정 타입에 대해 import 경로를 명시적으로 지정한다.
components
생성할 코드의 종류(모델, API 인터페이스 등)를 지정합니다.
cf. Open API 생성기를 통해서 클라이언트 또는 테스트 파일과 같은 다른 파일도 생성할 수 있다.
build.gradle에 다음 문장을 추가해준다.
compileJava.dependsOn swaggerSources.eStore.code
compileJava 작업이 실행되기 전에, OpenAPI Generator를 통해 코드 생성 작업(swaggerSources.eStore.code)이 먼저 실행되도록 설정한다.
cf. gradle build 과정
clean(기존 빌드 결과 삭제) -> compilejava(소스 코드를 컴파일하여 .class 파일 생성) -> processResource(리소스 파일을 복사하여 출력 디렉토리에 저장) -> classes(compileJava와 processResouces의 결과물을 결합하여 실행 가능한 클래스 생성) -> test(테스트 코드를 실행하고 결과를 출력) -> jar(컴파일된 클래스와 리소스 파일을 묶어 JAR 파일 생성) -> build(모든 작업을 마치고 최종 빌드 결과를 제공)
또한 processResources와 generateSwaggerCode를 연결해줘야 한다.
processResources {
dependsOn(generateSwaggerCode)
}
sourceSets.main.java.srcDir "${swaggerSources.eStore.code.outputDir}/src/main/java"
sourceSets.main.resources.srcDir "${swaggerSources.eStore.code.outputDir}/src/main/resources"
sourceSets는 Gradle에서 소스 코드와 리소스 파일의 경로를 정의하는 설정이다.
기본적으로 Gradle은 프로젝트의 소스 코드와 리소스 파일을 다음 경로에서 찾는다.
Java 코드: src/main/java/
리소스 파일: src/main/resources/
해당 경로 외에 다른 위치에 있는 소스 코드나 리소스 파일은 기본적으로 Gradle 빌드에 포함되지 않는다.
OpenAPI Generator가 생성한 파일들은 Gradle의 기본 소스 디렉토리가 아닌 경로(예: /build/swagger-code-eStore/)에 저장된다.
해당 파일들을 Gradle 빌드에 포함하지 않으면, 빌드 도중 "파일을 찾을 수 없음" 같은 컴파일 에러가 발생하게 된다.
cf. swaggerSources.eStore.code.outputDir는 Swagger Codegen이 생성한 코드가 저장될 디렉토리 경로를 정의하는 설정이다.
여태 설정한 명세를 기반으로 소스 파일을 생성하기 위해 빌드를 해준다.
gradlew clean build
빌드 후 다음과 같이 소스 파일(API 인터페이스)들이 생성된 것을 확인할 수 있다.
다음은 해당 코드에 적힌 주요 어노테이션들이다.
@Tag
@Tag(name = "Cart", description = "Everything about cart")
API 그룹을 지정한다.
@Operation
@Operation(
operationId = "addCartItemsByCustomerId",
summary = "Adds an item in shopping cart",
tags = { "cart" },
responses = {
@ApiResponse(responseCode = "201", description = "Item added successfully"),
@ApiResponse(responseCode = "404", description = "Given customer ID doesn't exist")
}
)
각각의 API 메서드를 설명하는 데 사용된다.
operationId
API 메서드의 고유 식별자이다.
summary
API 메서드의 간단한 설명이다.
tags
API 메서드가 속하는 태그 그룹(위의 @Tag와 연결됨)이다.
responses
응답 상태 코드와 설명을 정의한다.
@ApiResponse
@ApiResponse(responseCode = "201", description = "Item added successfully", content = {
@Content(mediaType = "application/xml", schema = @Schema(implementation = Item.class)),
@Content(mediaType = "application/json", schema = @Schema(implementation = Item.class))
}),
@ApiResponse(responseCode = "404", description = "Given customer ID doesn't exist")
API 호출에 대한 응답 코드와 설명을 정의한다.
responseCode
HTTP 상태 코드를 정의한다(예: 201, 404).
description
해당 응답 코드의 의미를 설명한다.
content
응답 데이터의 형식(JSON, XML 등)과 스키마를 지정한다.
@RequestMapping
@RequestMapping(
method = RequestMethod.POST,
value = "/api/v1/carts/{customerId}/items",
produces = { "application/xml", "application/json" },
consumes = { "application/xml", "application/json" }
)
API의 HTTP 메서드, 경로, 요청/응답 데이터 형식을 정의한다.
method
HTTP 메서드(예: POST, GET, PUT, DELETE)이다.
value
API 경로(예: /api/v1/carts/{customerId}/items)이다.
produces
API가 반환하는 데이터 형식이다(예: application/json, application/xml).
consumes
API가 수락하는 데이터 형식(예: application/json, application/xml)이다.
@Parameter
@Parameter(name = "customerId", description = "Customer Identifier", required = true)
@PathVariable("customerId") String customerId
API의 입력값(파라미터)을 설명한다.
name
파라미터 이름이다.
description
파라미터 설명이다.
required
필수 여부(true/false)이다.
@Schema
@Schema(implementation = Item.class)
API의 요청/응답 데이터 스키마를 정의한다.
개발자는 Swagger Codegen으로 스프링 코드를 작성해 API 인터페이스를 구현하고 그 안에 비즈니스 로직만 작성해주면 된다.
Swagger Codehen은 각 API 태그마다 인터페이스를 생성한다.
import com.packt.modern.api.CartApi;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Collections;
@RestController
public class CartsController implements CartApi {
private static final Logger log = LoggerFactory.getLogger(CartsController.class);
@Override
public ResponseEntity<List<Item>> addCartItemsByCustomerId(String customerId, @Valid Item item) {
log.info("고객 ID 요청: {}\nItem: {}", customerId, item);
return ok(Collections.EMPTY_LIST);
}
@Override
public ResponseEntity<List<Item>> getCartByCustomerId(String customerId){
throw new RuntimeException("수동 예외 발생 (Manual Exception thrown)");
}
}
addCartItemsByCustomerId
Item 요청(POST /api/v1/carts/{customerId}/items)을 추가하기 위해 addCartItemsByCustomerId 메서드 내부에서 들어오는 요청 페이로드와 고객 ID를 기록한다.
getCartByCustomerId
단순한 예외를 발생시킨다. 이를 통해 전역 예외 처리기(Global Exception Handler)의 결과를 확인할 수 있다.
cf. 전역 예외처리기
Spring Framework에서 제공하는 기능으로, 애플리케이션 전역에서 발생하는 예외를 하나의 클래스에서 처리할 수 있도록 해주는 구성 요소이다.
이를 통해 각 컨트롤러마다 따로 예외 처리를 작성하지 않고, 중앙에서 모든 예외를 관리할 수 있다.
컨트롤러에 작성된 각 메서들에서 발생하는 예외들을 처리할 중심 역할이 필요하고, 스프링은 이를 위해 AOP 기능을 제공한다.
@Controller가 달린 단일 클래스(Error 객체) 생성 후, @ExceptionHandler를 통해 예외 타입에 맞는 메서드를 정의하여 처리한다.
Lombok 라이브러리를 사용하면, getter, setter, constructor 등에 대한 코드의 복잡성을 제거할 수 있다.
@Setter
@Getter
public class Error {
private static final serialVersionUID = 1L;
private String ErrorCode;
private String message;
private Integer status;
private String url = "Not available";
private String reqMethod = "Not available";
}
serialVersionUID
직렬화된 객체가 역직렬화될 때 클래스의 버전을 검증하는 데 사용된다.
cf. 객체의 직렬화와 역직렬화 과정에서 버전 호환성을 보장한다. 클래스가 변경되었더라도 기존 데이터를 정상적으로 복원 가능하다.
미지정 시 자동으로 값이 생성되는데, 클래스가 조금만 변경돼도 달라지므로, 명시적으로 지정하는 것이 안전하다.
errorCode
HTTP 오류 코드와 다른 애플리케이션 오류 코드이다.
message
문제에 대한 간단하고 사람이 읽을 수 있는 요약이다.
status
문제 발생 시 서버에 의해 설정된 HTTP 상태 코드이다.
url
오류를 발생시킨 요청의 URL이다.
reqMethod
오류를 발생시킨 요청의 메서드이다.
cf. 필요 시 추가적인 필드도 설정 가능하다. Exceptions 패키지에는 사용자 정의 예외 및 전역 예외 처리를 위한 모든 코드가 포함된다.
ErrorCode Enum 정의
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public enum ErrorCode {
// Internal Errors: 1 to 0999
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"),
HTTP_MESSAGE_NOT_WRITABLE("PACKT-0003", "Missing 'Accept' header. Please add 'Accept' header."),
HTTP_MEDIA_TYPE_NOT_ACCEPTABLE("PACKT-0004", "Requested 'Accept' header value is not supported. Please use application/json or application/xml as 'Accept' value"),
JSON_PARSE_ERROR("PACKT-0005", "Make sure request payload should be a valid JSON object."),
HTTP_MESSAGE_NOT_READABLE("PACKT-0006", "Make sure request payload should be a valid JSON or XML object according to 'Content-Type'.");
private String errCode;
private String errMsgKey;
ErrorCode(final String errCode, final String errMsgKey) {
this.errCode = errCode;
this.errMsgKey = errMsgKey;
}
}
cf. enum 클래스에서는 생성자를 먼저 정의하면 오류가 발생한다.
에러 코드와 에러 메시지를 관리하기 위한 열거형(Enum)을 작성한다.
각 에러 상황에 대한 고유 코드와 메시지를 정의한다.
REST API에서 발생하는 다양한 예외를 처리할 때, 각 예외마다 반환할 에러 객체(Error 클래스)를 생성해야 하는데, 이 작업을 유틸리티 클래스로 분리하면 코드 중복 제거, 일관성 있는 에러 응답 제공, 유지보수 용이, 확장성 같은 장점이 존재한다.
package com.packt.modern.api.exceptions;
public class ErrorUtils {
private 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;
}
}
@ControllerAdvice
public class RestApiErrorHandler {
private static final Logger log = LoggerFactory.getLogger(RestApiErrorHandler.class);
private final MessageSource messageSource;
@Autowired
public RestApiErrorHandler(MessageSource messageSource) {
this.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);
}
...생략...
}
log
log 객체를 사용하면 애플리케이션 실행 중 발생하는 정보, 경고, 오류 등을 로그로 남길 수 있다.
@ControllerAdvice
애플리케이션의 모든 컨트롤러에서 발생하는 예외를 추적하고, @ExceptionHandler를 사용해 예외를 처리할 수 있다.
@ExceptionHandler()
특정 예외를 처리할 메서드에 붙이는 어노테이션이다. 괄호 안에 지정한 예외 클래스와 발생한 예외가 일치하거나, 하위 클래스인 경우 해당 메서드가 호출된다.
ex.printStackTrace();
예외 발생 시 스택 트레이스를 콘솔에 출력
cf. 스택 트레이스
프로그램 실행 중 발생한 오류나 예외 상황에서, 해당 오류가 발생하기까지의 메서드 호출 순서를 기록한 정보.
ResponseEntity<응답 본문 타입>(응답 본문, 헤더, 상태코드)
Spring Framework에서 HTTP 응답을 나타내는 클래스이다. 이를 사용하면 HTTP 상태 코드, 헤더, 본문(body) 등을 포함한 완전한 HTTP 응답을 구성하고 반환할 수 있다,
ResponseEntity로 객체를 반환하면, Spring은 해당 객체를 JSON으로 직렬화한다.
cf. 응답 본문의 객체가 public이 아니거나, 필드에 getter 메서드가 없는 경우 직렬화를 실패한다.
cf. pom.xml 또는 build.gradle에 Jackson 의존성이 추가되어 있어야 직렬화/역직렬화가 동작한다.
log.info()
Spring Boot에서 로그를 기록하는 Logger 객체의 info 레벨 메서드이다.
기본적으로 로그는 터미널(콘솔)에만 출력되며, 파일에는 저장되지 않기 때문에 로그 파일을 application.properties에서 따로 설정해줘야한다.
ex. logging.file.name=logs/application.log
.gradlew clean build
java -jar .\build\libs\modernAPI-0.0.1-SNAPSHOT.jar
위 명령어를 통해 애플리케이션을 빌드하고, 실행시켜준다.
PostMan을 통해 API를 테스트 해준다.
GET http://localhost:8080/api/v1/carts/1 header: Accept: application/xml
GET http://localhost:8080/api/v1/carts/1 header: Accept: application/json
POST http://localhost:8080/api/v1/carts/1/items Accept: application/json, ContentType: application/json
위 내용들과 같이 api들이 정상적으로 동작하는 것을 확인할 수 있다.