๋ณธ ๊ธ์, ํ์๊ฐ Swagger๋ฅผ ์ฌ์ฉํ๋ฉด์ ๊ณ์๋์ด ์ฌ์ฉ๋ฒ์ด ์ ๋ฐ์ดํธ ๋ ์์ ์ ๋๋ค.
- 2023.03.24 : Tag / Operation / ApiResponses / ApiResponse / Parameter
- 2023.03.25 : @ControllerAdvice์์์ ์ฌ์ฉ, @Schema, @Schema์ hidden ์ต์ ๊ณผ ๋ฌธ์ ํด๊ฒฐ
๐ 0. ์๋ก
๋ฐฑ์๋ ๊ฐ๋ฐ์๊ฐ REST API๋ฅผ ์ ๊ฐ๋ฐํ๋ ๊ฒ์ ํ์์ ์์์ ๋๋ค. ์ด๋ ๊ฒ ์ ๋ง๋ API์ ๋ํด ์ ์ ๋ฆฌํ ๋ฌธ์๊ฐ ์๋ค๋ฉด, ์ฌ์ฉํ๋ ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์์, ์ธ์์ธ๊ณ๋ฅผ ๋ฐ๊ฑฐ๋, ์ฌ๋ด API๋ฅผ ํธ์ถํ EndPoint์ ๋ํด ๊ถ๊ธํดํ๋ ๋ค๋ฅธ ๋ฐฑ์๋ ๊ฐ๋ฐ์์๊ฒ ๊ต์ฅํ ์ ํจํ ์์์ผ๋ก ๋จ๊ธธ ์ ์์ต๋๋ค!
ํ์ ๋๋, API ๋ฌธ์๋ฅผ ๋ฐ๋ก ์ธ๋ถ ๋ฌธ์๋ก ์์ฑํด์ ์ ์ฅ์ ํ์ผ๋ ๊ฐ๋ฐ์ ์งํํ ๋๋ง๋ค ์ต์ ํ๋ฅผ ํด์ฃผ๋ ๊ณผ์ ์์ ๊ต์ฅํ ๋ถํธํจ์ ๋๊ผ์ต๋๋ค. ๊ทธ ์ค์ ํด๋จผ์๋ฌ๊ฐ ๋ฐ์ํด์ ์ํต์ ์ค๋ฅ๊ฐ ์๊ฒผ๋ ๊ฒฝํ๋ ์์ต๋๋ค.
์ฐ๋ฆฌ๋ ์ฝ๋๋ฅผ ์์ฑํ๋ ์ฌ๋์ ๋๋ค. ์ฝ๋๋ ๋ฒจ์์ API ๋ฌธ์๋ฅผ ์๋ํํ์ฌ ์์ฑํ ์ ์๊ณ , ์ฝ๋ ๋ ๋ฒจ์์ ๋ฌธ์๋ฅผ ์ต์ ํ ํ ์ ์๋ค๋ฉด ์ผ๋ง๋ ์ข์๊น์? ๋ํ ํด๋ผ์ด์ธํธ๊ฐ input value๋ฅผ ๋ฃ์ด ๋ฌธ์์์์ ํ ์คํธ๋ฅผ ์งํํ ์ ์๋ค๋ฉด ์ผ๋ง๋ ์ข์๊น์?
- REST API ๋ฅผ ์ค๊ณ, ๋น๋, ๋ฌธ์ํ ๋ฐ ์ฌ์ฉํ๋๋ฐ ๋์์ด ๋๋ OpenAPI ์ฌ์์ ์ค์ฌ์ผ๋ก ๊ตฌ์ถ๋ ์คํ ์์ค ๋๊ตฌ ์ธํธ
- ์ฝ๋ ๋ช ์ค ์ถ๊ฐ๋ฅผ ํตํด ์ ์ฉํ๊ธฐ ์ฌ์ฐ๋ฉฐ, ๋ฌธ์ ํ๋ฉด์์ UI๋ฅผ ํตํด ๋ฐ๋ก API ํ ์คํธ๊ฐ ๊ฐ๋ฅํ ์ฅ์
์์ ๊ฐ์ ์ฅ์ ์ ๊ฐ์ง๊ณ ์๋ Swagger๋ฅผ ํตํด ์ด๋ฌํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด๋ด ์๋ค!
๐ค 1. Swagger ํ๋ก์ ํธ์ ์ ์ฉ
1-1. Springdoc vs SpringFox
Swagger๋ ๋ ๊ฐ์ง ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ ํํ ์ ์์ต๋๋ค. ํ๋๋ Springdoc์ด๊ณ , ํ๋๋ SpringFox์ ๋๋ค.
[springdoc] https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui
[springfox] https://mvnrepository.com/artifact/io.springfox/springfox-swagger2
Springfox์ Springdoc์ ์ฐจ์ด๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
Springfox | Springdoc | |
์ ๋ฐ์ดํธ ์ต์ ํ | Jul 14, 2020 (3.0.0 version) | Mar 09, 2023 (1.6.15 version) |
webflux ์ง์ | X (์ ๋ฒ์ ์ ์ง์.) | O |
๋น๊ณ | ํ๋์ ์ ๋ฐ์ดํธ๊ฐ ๋๊ฒผ๋ค๊ฐ, ๊ฐ๊ฐํ ์ ๋ฐ์ดํธ | ํ๋ฐ์ฃผ์, Springfox ๊ฐ์ |
๊พธ์คํ ์ ๋ฐ์ดํธ์ ์ข ๋ ๋ง์ ๋ ํผ๋ฐ์ค๋ ์๋ฃ๋ฅผ ์ ๊ณตํ๊ณ ์๋ Springdoc์ด ์ข ๋ ๋ซ๋ค๋ ์๊ฒฌ์ด ๋ง์์ต๋๋ค. ๊ธฐ์กด์ Springfox๋ฅผ ์ฌ์ฉํ๋ ์ฌ๋๋ค์๊ฒ ์ด์ฃผ๋ฅผ ์ํค๊ธฐ ์ํ ๋ฐฉ๋ฒ๋ Springdoc์ ์น์ ํ ๋์์์์ต๋๋ค.
๋ฐ๋ผ์ ์ ๋ Springdoc์ ์ด์ฉํด์ ์คํ๋ง ํ๋ ์์ํฌ์์ ์ฝ๊ฒ Swagger๋ฅผ ์ฌ์ฉํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
1-2. build.gradle ์ค์
- spring fox
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.springfox:springfox-swagger-ui:3.0.0'
- spring doc
implementation 'org.springdoc:springdoc-openapi-ui:1.6.15'
์ ๋ Springdoc์ ์ฌ์ฉํด์ swagger๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค.
๊ฐ๋จํ๊ฒ ์์กด์ฑ๋ง ์ถ๊ฐํ ํ,
- http://localhost:8080/swagger-ui.html
์์ ๋งํฌ๋ฅผ ํตํด ์ ์ํ๋ฉด(ํฌํธ, ip ๋ฐ๋ก ์ง์ ์ ํ ๊ฒฝ์ฐ) ์๋์ ๊ฐ์ด Swagger UI๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.

1-3. application.yml ์ค์
OpenAPI 3 Library for spring-boot
Library for OpenAPI 3 with spring boot projects. Is based on swagger-ui, to display the OpenAPI description.Generates automatically the OpenAPI file.
springdoc.org
์์ ๋งํฌ์ ๋ค์ด๊ฐ๋ฉด, application.yml์ ์ค์ ํ๋ ํ๋กํผํฐ์ ๋ํด ๋์์์ผ๋, ์์ ์ด ํ์ํ ๊ฐ์ ๋ํด์ ์ต์ ์ ์กฐ์ ํ๋ฉด ๋ฉ๋๋ค.
springdoc:
override-with-generic-response: true
default-consumes-media-type: application/json # ์๋น ๋ฏธ๋์ด ํ์
default-produces-media-type: application/json # ์์ฐ ๋ฏธ๋์ด ํ์
swagger-ui:
operations-sorter: method # operations ์ ๋ ฌ ๋ฐฉ์์ HTTP Method ์
tags-sorter: alpha # tag ์ ๋ ฌ ๋ฐฉ์์ ์ํ๋ฒณ ์
path: "swagger.html" # http://localhost:8080/swagger.html๋ก ์ ์ ๊ฐ๋ฅ
์ ๋ ์กฐ๊ธ ๋ ์ฌ์ฉํ๋ฉด์ ํ์ํ ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํ๊ธฐ ์ํด, ๊ธฐ๋ณธ์ ์ผ๋ก ์์ ๊ฐ์ ์ค์ ์ ์งํํ์ต๋๋ค.
๐ค 2. Swagger ์ฌ์ฉํ๊ธฐ
0. Swagger2 โ Swagger3์ ๋ณํ, Config ํ์ผ ์์ฑํ๊ธฐ
- ์ด๋ ธํ ์ด์ ์ ๋ง์ ๋ณํ๊ฐ ์๊ฒจ, Swagger3๋ก ๋ณํํ ์ดํ์๋ ์๋์ ํด๋นํ ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
@ApiParam โ @Parameter
@ApiOperation โ @Operation
@Api โ @Tag
@ApiImplicitParams โ @Parameters
@ApiImplicitParam โ @Parameter
@ApiIgnore โ @Parameter(hidden = true) / @operation(hidden = true) / @hidden
@apimodel โ @Schema
@ApiModelProperty โ @Schema
Swagger UI๋ฅผ ๊ตฌ์ฑํ๊ธฐ ์ํ Config ํ์ผ์ ํ๋ ์์ฑํด์ค๋๋ค.
@Configuration
public class GTOpenApiConfig {
@Bean
public OpenAPI openAPI() {
Info info = new Info()
.version("v1.0.0")
.title("GT(God Tong) API")
.description("God Tong (๊ฐํต) ํฌ๊ณผ ์คํ์ ์์ ๋ก์ด ๋ง๋จ");
return new OpenAPI()
.info(info);
}
}
์์ ๊ฐ์ ๊ฐ๋จํ ๋ฉ์๋๋ฅผ ๋ง๋ค์ด ๋น์ผ๋ก ๋ฑ๋กํด์ค์๋ค.
version๊ณผ, title, description์ ๊ฐ์์ ํ๋ก์ ํธ์ ๋ง๊ฒ ์์ฑํ๋ฉด ๋ฉ๋๋ค.
1. @Tag์ @Operation
@Tag(name = "xxx", description = "xxx")
public class TestController{
// ...
}
@Tag๋ API์ ๊ทธ๋ฃน ์ค์ ์ ์ํ ํ๊ทธ์ ๋๋ค. REST API์ EndPoint๋ ์ปจํธ๋กค๋ฌ์ ์์ผ๋ฏ๋ก, ํด๋น ์ปจํธ๋กค๋ฌ ํด๋์ค ์์ @Tag ์ด๋ ธํ ์ด์ ์ ํตํด API์ ๊ทธ๋ฃน์ ์ค์ ํ ์ ์์ต๋๋ค.
์์ ์์ ์ฝ๋ ์ฒ๋ผ, name๊ณผ description ํ๋กํผํฐ๋ฅผ ํตํด ๊ฐ์ ์ปค์คํ ํ ์ ์์ต๋๋ค.
์๋๋, name = "GT Member", description = "GT์ ํ์ ๊ด๋ จ~"์ผ๋ก ์ค์ ํ ํ Swagger UI๋ฅผ ํ์ธํ ๋ชจ์ต์ ๋๋ค.

API์ ๊ทธ๋ฃน์ธ 'GT Member'๋ฅผ ํ์ธํ ์ ์๊ณ , ์์ ์์ ํฐํธ๋ก ์ค๋ช ์ธ description์ด ๋ฌ๋ ค์๋ ๋ชจ์ต์ ๋ณผ ์ ์์ต๋๋ค.
์ด์ @Tag ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ํด๋์ค ๋ด๋ถ์ ๊ฐ @Operation ์ด๋ ธํ ์ด์ ์ด ๋ถ์ด์๋ ๋ฉ์๋๋ฅผ ํ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด๊ฒ ํด์ค๋๋ค.
@Tag(name = "xxx", description = "xxx")
public class TestController{
@Operation(summary = "xxx", description = "xxx")
@GetMapping("/test")
public void test(){
}
}
@Operation ์ด๋ ธํ ์ด์ ์, ๊ฐ API์ ๋ํ ์์ธ ์ค๋ช ์ ๊ธฐ์ฌํ ์ ์๋ ์ด๋ ธํ ์ด์ ์ ๋๋ค. ํด๋น ์ด๋ ธํ ์ด์ ์์ ์ค์ํ ํ๋กํผํฐ๋ summary์ description์ ๋๋ค.
๋ ๋ค String Type์ผ๋ก ์ ๋ ฅ์ด ๊ฐ๋ฅํ๊ณ , ์์๋ก summary = "ํ์ ๊ฐ์ (joinIn)", description = "GT ์๋น์ค์ ํ์๊ฐ์ ~"์ ๊ฐ์ด ์ ๋ ฅํ๋ค๋ฉด, Swagger UI์๋ ๋ค์๊ณผ ๊ฐ์ด ์ถ๋ ฅ๋ฉ๋๋ค.

๋ํ @Operation ์ด๋ ธํ ์ด์ ์ parameters ํ๋กํผํฐ๋ฅผ ์ฌ์ฉํด์, Swagger UI์ ํ๋ผ๋ฏธํฐ ์ ๋ ฅ ์ฐฝ์ ๋์ธ ์ ์์ต๋๋ค.
@Operation(summary = "ํ์ ๊ฐ์
(joinIn)", description = "GT ์๋น์ค์ ํ์๊ฐ์
ํฉ๋๋ค. GT ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ ๊ณตํ๋ ํ์๊ฐ์
์๋น์ค์
๋๋ค.",
parameters = {@Parameter(name = "GTJoinInRequest", description = "ํ์๊ฐ์
์์ฒญ ๊ฐ์ฒด")})
์์ ๊ฐ์ ์ด๋ ธํ ์ด์ ์ ๋ฉ์๋์์ ๋ถ์ด๋ฉด, ์๋์ ๊ฐ์ ํ๋ฉด์ด ๋์ต๋๋ค.

์์๋ ๋ค๋ฅด๊ฒ ์๋ก์ด ํ๋ผ๋ฏธํฐ๊ฐ ์๊ฒผ์ต๋๋ค! ์์ Try it out์ ํด๋ฆญํ๋ฉด, ํด๋น ๋น ์นธ์ ๊ฐ์ ๋ฃ์ด ํ ์คํธ ํด๋ณผ ์ ์๋ ์์คํ ๋ ๊ฐ์ถฐ์ ธ ์์ต๋๋ค.
2. @ApiResponses์ @ApiResponse
์์ฒญ์ ์์ @Operation ๋ด๋ถ์ @Parameter๋ฅผ ํตํด ๋ํ๋ผ ์ ์์์ต๋๋ค. ์ด์ ๋ถํฐ, ํด๋น API๊ฐ ๋ด๋ณด๋ด๋ Response๋ฅผ API ๋ฌธ์์ ์ ์ด๋ณด๋๋ก ํฉ์๋ค.
API์ ์๋ต์ ์ฌ๋ฌ๊ฐ์ง ์ผ์ด์ค๊ฐ ์์ต๋๋ค. ์์ฒญ์ ๋น์ฆ๋์ค ๋ก์ง์ ๋ง์ถ์ด ์ ์ฒ๋ฆฌ๋ฅผ ํ์ ๋, ์ฌ๋ฐ๋ฅด์ง ์์ ์์ฒญ์ด ๋ค์ด์์ ๋, ์๋ฒ ๋ด๋ถ์ ์ธ ์๋ฌ์ ์ํด ๋น์ฆ๋์ค ๋ก์ง์ ์์ํ์ง ๋ชปํ์ ๋ ๋ฑ, ๋ค์ํ ์ํฉ์ ๋ง์ถฐ ์ด๋ค ๋ฐฉ์์ผ๋ก ์๋ต์ด ์ค๋์ง ๊ธฐ๋กํด์ผ ํ ํ์์ฑ์ด ์์ต๋๋ค.
์ด์ ๋ง์ถฐ Springdoc์์๋ @ApiResponses๋ฅผ ์ง์ํฉ๋๋ค.
@ApiResponses(value = { ... })
@Target({METHOD, TYPE, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ApiResponses {
ApiResponse[] value() default {};
Extension[] extensions() default {};
}
value์ ํ์ ์ ApiResponse[]์ด๋ฏ๋ก, value = {...} ๋ถ๋ถ์ @ApiResponse()๋ฅผ ์ฌ๋ฌ ๊ฐ ์ฝ์ ํ๋ ๋ฐฉ์์ผ๋ก ๊ตฌํํ ์ ์์ต๋๋ค.
์ฆ, @ApiResponses๋ ์ฌ๋ฌ @ApiResponse๋ฅผ ํ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด์ฃผ๋ ์ญํ ์ ํฉ๋๋ค.
์ด์ ๊ฐ๊ฐ์ ์๋ต์ ๋ํด ์ ์ด๋ด ์๋ค!
@Target({METHOD, TYPE, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Repeatable(ApiResponses.class)
public @interface ApiResponse {
String description() default "";
String responseCode() default "default";
Header[] headers() default {};
Link[] links() default {};
Content[] content() default {};
Extension[] extensions() default {};
String ref() default "";
boolean useReturnTypeSchema() default false;
}
@ApiResponse๋ description๊ณผ responseCode๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
responseCode๋ HttpStatus์ ์ฝ๋๋ฅผ ์ ์ผ๋ฉด ๋ฉ๋๋ค. (ex. 200, 404, 500 ๋ฑ)
description์ ํด๋น ์๋ต์ ๋ํ ์ค๋ช ์ ๊ธฐ์ฌํ๋ฉด ๋ฉ๋๋ค.
ํด๋น Response์ Header, Link, Content์ ๋ํด์๋ ๋ฌธ์์ ๊ธฐ๋กํ ์ ์์ผ๋ฏ๋ก, ํ์ ์ ํด๋น ์ด๋ ธํ ์ด์ ์ ์์ฑํ๋ฉด์ ์ถ๊ฐํ๋ฉด ๋ฉ๋๋ค.
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful Join Us!"),
@ApiResponse(responseCode = "400", description = "Bad Request")
})
์ปจํธ๋กค๋ฌ์ ๋ฉ์๋ ์์ ์์ ๊ฐ์ ์ด๋ ธํ ์ด์ ์ ๋ถ์๋ค๊ณ ๊ฐ์ ํด ๋ด ์๋ค.
๊ทธ๋ ๋ค๋ฉด, Swagger UI์์๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ฉด์ ๋ณผ ์ ์์ต๋๋ค.

์ด๋ฌํ ๋ด์ฉ์ @ControllerAdvice์์๋ ๋ง์ฐฌ๊ฐ์ง์ ๋๋ค.
@ExceptionHandler๊ฐ ๋ถ์ ๋ฉ์๋์, ํ๋์ ์์ฒญ ์๋ต ์ฝ๋์ ๋ํ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๋ค๋ฉด @ResponseStatus ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด ์ ์ํ๋ฉด ์๋์ผ๋ก ํด๋น ์๋ฌ์ ๋ํ ์๋ต์ด Swagger UI์ ๋ฐ์๋ฉ๋๋ค.
ํ์ง๋ง, ๋ง์ฝ ErrorResponse์ ๋ํด Custom์ผ๋ก ์ ์ํ๊ณ , ์ํฉ์ ๋ฐ๋ผ ์๋ฌ์ฝ๋๊ฐ ๋ณํ๋๋ค๋ฉด ์ด๋จ๊น์?
@ResponseStatus๋ constantํ ๊ฐ์ ์ํ๊ธฐ ๋๋ฌธ์ ํ๋์ ์ด๋ ธํ ์ด์ ์ผ๋ก ์ ์ํ ์ ์์ต๋๋ค.
์ด๋์๋ ๋ง์ฐฌ๊ฐ์ง๋ก @ApiResponses์ @ApiResponse๋ฅผ ์ฌ์ฉํด์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
@ExceptionHandler(value = {GTApiException.class})
@ApiResponses(value = {
@ApiResponse(responseCode = "400", description = "Bad Request!"),
@ApiResponse(responseCode = "403", description = "Forbidden! Already Exist"),
})
public ResponseEntity<GTErrorResponse> handleExceptionFromAPIMethod(GTApiException ex){
GTErrorCode errorCode = ex.getErrorCode();
return handleExceptionInternal(errorCode);
}
์์ ๊ฐ์ด ์ปค์คํ ํ 'GTApiException'์ ๋ํด ํธ๋ค๋งํ๊ณ ์๋ ๋ฉ์๋์ ๋ํด, ๋ฐ์ํ Exception์ Enum ๊ฐ์ ๋ฐ๋ผ ๋ค์ํ HTTP Status ์ฝ๋๋ฅผ ๊ฐ๋ ์ํฉ์ ๋๋ค. ์ด๋, @ApiResponse๋ฅผ ์ฌ์ฉํด์ ํด๋น ์๋ฌ ์ฝ๋๋ค์ ๋ํด responseCode์ description์ ์ฌ์ฉํ๋ฉด ์ด ๋ด์ฉ์ Swagger UI์ ์ ๋ฐ์ํ ์ ์์ต๋๋ค.

์ด์ ๊ฐ์ด ์ ๋ฐ์๋ ๋ชจ์ต์ ํ์ธํ ์ ์์ต๋๋ค.
3. @Schema
์์ง ๋ถ์กฑํ ์ ์ด ๋ ์์ต๋๋ค. Request Body์ Response์ Example Value์ Schema์, ๊ฐ ํ๋์ ๋งค์นญ๋๋ ํ์ ๋ง ๊ธฐ์ฌ๋์ด ์์ต๋๋ค. ํ์ ๋ฟ๋ง ์๋๋ผ ์ ํํ ์์ ๋ฐ์ดํฐ๋ ํด๋น ํ๋์ ๋ํ ์์ธ ์ค๋ช ์ด ๋ถ๊ฐ์ ์ผ๋ก ๋ค์ด๊ฐ ์๋ค๋ฉด ๋ ์ข์ API ๋ฌธ์๊ฐ ๋ ๊ฒ์ ๋๋ค.
์ด๋ ๊ฒ Request์ Response DTO์ ๋ํด ๋ช ์ธ๋ฅผ ์ ์ ์ ์๋ ์ด๋ ธํ ์ด์ ์ด ๋ฐ๋ก @Schema์ ๋๋ค.
@Schema(description = "ํ
์คํธ์ฉ DTO")
public class ExampleDTO{
@Schema(description = "ํ
์คํธ ๊ฐ", example = "abcd")
public String value;
}
์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก @Schema ์ด๋ ธํ ์ด์ ์ ํ์ฉํ ์ ์์ต๋๋ค.
- ํด๋์ค์ ์ด๋ ธํ ์ด์ : ํด๋น Request / Response ๊ฐ์ฒด์ ๋ํด ์ ์, Swagger UI์ 'schema' ํญ์์ ํ์ธ ๊ฐ๋ฅ
- ํ๋์ ์ด๋ ธํ ์ด์ : ํด๋น ํ๋์ ๋ํ ์ค๋ช , ์์ ๋ฐ์ดํฐ์ ๋ํด ์ค์ ๊ฐ๋ฅ.
Response ๊ฐ์ฒด ์ญ์ @Schema ์ด๋ ธํ ์ด์ ์ ๋๊ฐ์ด ํ์ฉํ๋ฉด ๋์ง๋ง, Swagger UI์ ์ ์ฉํ๊ธฐ ์ํด์ ํ ๋จ๊ณ ๋ ๊ฑฐ์ณ์ผ ํฉ๋๋ค.
@ApiResponse์ 'content' ํ๋กํผํฐ์, @Content ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ ์ปจํ ์ธ ๋ฅผ ์ฝ์ ํด์ผ ํฉ๋๋ค.
// ... ์๋ต
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful Join Us!",
content = @Content(schema = @Schema(implementation = GTJoinInResponse.class))),
})
public void test(){}
์์ ๊ฐ์ด @Schema ์ด๋ ธํ ์ด์ ์ผ๋ก ๊ตฌ์ฑํ Response ํด๋์ค๋ฅผ implementationํ ์ปจํ ์ธ ๋ฅผ ์์ฑํ๋ฉด, ์ต์ข ์ ์ผ๋ก Swagger UI์๋ Response์ ๋ํ ๋ช ์ธ๋ ๋ฐ์์ด ๋ฉ๋๋ค.

4. @Schema์ hidden ์ต์ ๊ณผ ๋ฌธ์ ํด๊ฒฐ
@Schema์๋ hidden ์ต์ ์ด ์์ด์ hidden = true ์ต์ ์ ์ฃผ๊ฒ ๋๋ฉด, Swagger UI์ ์ถ๋ ฅ๋์ง ์์ต๋๋ค.
๊ทธ๋ฌ๋ ์ ๋ ๋ค์๊ณผ ๊ฐ์ ์ํฉ์ ๋ง์ฃผ์ณค์ต๋๋ค.
@Getter
@Builder
@Schema(description = "GT์ ์๋ฌ ์๋ต ๊ฐ์ฒด")
@RequiredArgsConstructor
public class GTErrorResponse {
@Schema(description = "GT ์ปค์คํ
์๋ฌ์ฝ๋", example = "INVALID_PARAMETER")
private final String code;
@Schema(description = "GT ์๋ฌ ์์ธ๋ฉ์์ง๋ก, ErrorCode ๋ด์ฅ ๋ฉ์์ง + Exception ๋ฉ์์ง๊ฐ ํฉ์ณ์ง ํํ",
example = "Invalid parameter included :: ๋์ด(age) ํ๋์๋ 1 ์ด์์ ์ซ์๊ฐ ์
๋ ฅ๋์ด์ผ ํฉ๋๋ค.")
private final String message;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Schema(description = "Validation Error ๋ฐ์์ ์์ธ ํ๋ ์๋ฌ ๊ธฐ์ฌ", nullable = true)
private final List<ValidationError> errors;
@Getter
@Builder
@RequiredArgsConstructor
public static class ValidationError {
// ...
}
}
์ด๋ฌํ ErrorResponse๋ฅผ ๊ตฌํํ๊ณ , List<ValidationError>๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์งํํ์ ๋ ์ด๋ค ํ๋์์ ์๋ฌ๊ฐ ์ฐํ๋์ง ์์ธํ๊ฒ ๋ํ๋ด๊ธฐ ์ํด์ ํ๋์ ํฌํจ์์ผฐ์ต๋๋ค.
ํ์ง๋ง, JsonInclude.Include.NON_EMPTY๋ฅผ ์ฌ์ฉํด์ List๊ฐ ๋น์ด์์ง ์์ ๊ฒฝ์ฐ์ Json์ ํฌํจ์ํค๋๋ก ์ค์ ํ์ต๋๋ค.
์ฌ๊ธฐ๊น์ง ์ค์ ์ ํ๋ค๋ฉด, Swagger UI์์๋ ์ด๋ฌํ ์กฐ๊ฑด์ ํ๋จํ๊ธฐ ์ด๋ ค์ ๋ชจ๋ ์ถ๋ ฅํ๊ฒ ๋ฉ๋๋ค.

์์ ์์์์ 400์๋ฌ์ 403 ์๋ฌ๋ฅผ ๋ณด๋๋ก ํฉ์๋ค. ๋ ๋ค return ๊ฐ์ GTErrorResponse๋ก ๋์ผํฉ๋๋ค.
ํ์ง๋ง, Validation ์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฐ๊ณผ๋ก 400 ์๋ฌ๋ฅผ ์ถ๋ ฅํ ์์ ์ด๋ฏ๋ก, 400 ์๋ฌ์ ๋ํด์๋ ๊ทธ๋๋ก ์ถ๋ ฅํ๊ณ ,
403 ์๋ฌ์ ๋ํด์๋ "errors" ํ๋๋ฅผ ์ ๊ฑฐํ๊ณ ์ถ์ ์ํฉ์ ๋๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ์ ์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
@Schema์ hidden ๊ฐ์ ์กฐ์ ํ๊ฒ ๋๋ฉด, ๋ชจ๋ ์๋ฌ์ ๋ํด ๋ฐ์๋๋ฏ๋ก hidden์ ์ฌ์ฉํ๊ธด ์ด๋ ค์ ๋ณด์ ๋๋ค.
์ ๊ฐ ์ ํํ ๋ฐฉ๋ฒ์, @JsonView ์ ๋๋ค.
@JsonView๋ ๋์ผํ POJO ์ค๋ธ์ ํธ์ ๋ํด์ ์ ํ์ ์ผ๋ก ์๋ก ๋ค๋ฅธ ํ๋ผํผํฐ๊ฐ ์กฐํฉ๋ JSON ๋ฌธ์์ด์ ๋ง๋ค ์ ์์ต๋๋ค.
๋ฐ๋ผ์, ์์ List<ValidationError>์ ๋ํด์๋ 400 Error์ Response์์๋ง ๋ณด์ด๋๋ก ์กฐ์ ํ ์ ์์ต๋๋ค.
1. JsonView Class ์์ฑ
public class GTJsonView {
public static class Common{}
public static class Hidden extends Common{}
}
์์ ๊ฐ์ JsonView ํด๋์ค๋ฅผ ์์ฑํ์ต๋๋ค. ๋จ์ง ํด๋์ค๋ฅผ ๊ตฌ๋ถํ๋ ์ญํ ์ด๋ฏ๋ก, public static ํค์๋๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
Common์ ๊ณตํต์ ์ผ๋ก Json์ ํฌํจ๋๋ ๋ด์ฉ์ด๋ฉฐ, Hidden์ Common๊ณผ ํจ๊ป ์ถ๊ฐ๋ก Json์ ํฌํจ๋์ด์ผ ํ๋ ๋ด์ฉ์ด๋ฏ๋ก extends๋ฅผ ์ด์ฉํด ์์ํด์คฌ์ต๋๋ค.
2. @JsonView ์ค์
@Getter
@Builder
@Schema(description = "GT์ ์๋ฌ ์๋ต ๊ฐ์ฒด")
@RequiredArgsConstructor
public class GTErrorResponse {
@Schema(description = "GT ์ปค์คํ
์๋ฌ์ฝ๋", example = "INVALID_PARAMETER")
@JsonView(GTJsonView.Common.class)
private final String code;
@Schema(description = "GT ์๋ฌ ์์ธ๋ฉ์์ง๋ก, ErrorCode ๋ด์ฅ ๋ฉ์์ง + Exception ๋ฉ์์ง๊ฐ ํฉ์ณ์ง ํํ",
example = "Invalid parameter included :: ๋์ด(age) ํ๋์๋ 1 ์ด์์ ์ซ์๊ฐ ์
๋ ฅ๋์ด์ผ ํฉ๋๋ค.")
@JsonView(GTJsonView.Common.class)
private final String message;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonView(GTJsonView.Hidden.class)
@Schema(description = "Validation Error ๋ฐ์์ ์์ธ ํ๋ ์๋ฌ ๊ธฐ์ฌ", nullable = true)
private final List<ValidationError> errors;
@Getter
@Builder
@RequiredArgsConstructor
public static class ValidationError {
/ ...
}
}
์ด์ ๊ฐ ํ๋์ @JsonView๋ฅผ ์ค์ ํด์ค๋๋ค. value๋ฅผ Class<?>[] ํ์ ์ ๊ฐ๊ธฐ ๋๋ฌธ์ ๋ง๋ ๋ฐฉ๊ธ ๋ง๋ค์ด์ค JsonView class๋ฅผ ์ ๋ ฅํด์ฃผ๋ฉด ๋ฉ๋๋ค.
์ด์ ์ปจํธ๋กค๋ฌ์ ๋ฉ์๋์๋ ์ ์ฉํด๋ด ์๋ค.
@ExceptionHandler(value = {GTApiException.class})
@ApiResponses(value = {
@ApiResponse(responseCode = "403", description = "Forbidden! Already Exist ID or Member"),
})
@JsonView(GTJsonView.Common.class)
public ResponseEntity<GTErrorResponse> handleExceptionFromGTMemberController(GTApiException ex){
GTErrorCode errorCode = ex.getErrorCode();
String detailErrorMessage = ex.getMessage();
return handleExceptionInternal(errorCode, detailErrorMessage);
}
์ด ๋ถ๋ถ์๋ Common ์ฒ๋ฆฌ๋ฅผ ํ์ต๋๋ค. Hidden์ ์ํ ํ๋๋ ๋ณด์ด์ง ์๊ฒ ์ฃ .
@ControllAdvice์ 400 ์๋ฌ๊ฐ ์๊ธฐ ๋๋ฌธ์, ์ฌ๊ธฐ์ ์ฝ๋๋ ๋ฐ์ทํด ๋ณด๊ฒ ์ต๋๋ค.
@Override
@ApiResponses(value = {
@ApiResponse(responseCode = "400", description = "Bad Request!",
content = @Content(schema = @Schema(implementation = GTErrorResponse.class))),
})
@JsonView(GTJsonView.Hidden.class)
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
log.warn("handleIllegalArgument", ex);
GTErrorCode errorCode = GTCommonErrorCode.INVALID_PARAMETER;
return handleExceptionInternal(errorCode, ex);
}
์ฌ๊ธฐ๋ Hidden๊น์ง ํ์ฉํ๋ฏ๋ก, Swagger UI์์ ํ์ธํ ์ ์์ต๋๋ค.
์ด์ ๊ฒฐ๊ณผ๋ฅผ ํ์ธํด๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.

๐โโ๏ธ ์ถ์ฒ
[๋งํฌ] https://velog.io/@ychxexn/Swagger-%EB%8B%A4-%EA%B0%99%EC%9D%80-%EB%86%88%EC%9D%B4-%EC%95%84%EB%8B%88%EC%97%88%EB%8B%A4.-Springfox-vs-Springdoc
[๋งํฌ] https://www.javatpoint.com/swagger
'๐ฑ ๋ฐฑ์๋ : Backend' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
- ๐ 0. ์๋ก
- ๐ค 1. Swagger ํ๋ก์ ํธ์ ์ ์ฉ
- 1-1. Springdoc vs SpringFox
- 1-2. build.gradle ์ค์
- 1-3. application.yml ์ค์
- ๐ค 2. Swagger ์ฌ์ฉํ๊ธฐ
- 0. Swagger2 โ Swagger3์ ๋ณํ, Config ํ์ผ ์์ฑํ๊ธฐ
- 1. @Tag์ @Operation
- 2. @ApiResponses์ @ApiResponse
- 3. @Schema
- 4. @Schema์ hidden ์ต์ ๊ณผ ๋ฌธ์ ํด๊ฒฐ
- ๐โโ๏ธ ์ถ์ฒ