ํ์ ์ ์ํด Swagger ์ข ๋ ์ ์ฌ์ฉํด๋ณด๊ธฐ
๋ชฉ์ฐจ
- API ๊ทธ๋ฃนํ
- API ๋ฒ์ ๊ด๋ฆฌ ์ @Deprecated ํ์ฉํ๊ธฐ
- ๋ช ์ธ๋ง ๋จผ์ ์ ๋ฌํ๊ธฐ
- Authorize์ jwt ๋ฃ์ ๋ prefix์ Bearer ์๋ต์ํค๊ธฐ
- ๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ์๋ ์ธ์ฆ์ ๋ณด ์ ์ง์ํค๊ธฐ
- ๊ธฐํ ์์ํ ์์ธ ์ค์ ๋ค
1. API ๊ทธ๋ฃนํ
Swagger์์ API๋ฅผ ๊ทธ๋ฃนํํ๋ฉด ์ฌ๋ฌ ์๋ํฌ์ธํธ๋ฅผ ๋ ผ๋ฆฌ์ ์ธ ๊ทธ๋ฃน์ผ๋ก ๋ฌถ์ด ๊ด๋ฆฌํ๊ณ ๋ฌธ์๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ์ ๋ฆฌํ ์ ์์ต๋๋ค.
์ค์ ํ์๋ ์ ์ด๋ฏธ์ง์ฒ๋ผ ๊ด๋ จ๋ ์๋ํฌ์ธํธ๋ง ๋ณผ ์ ์์ด ์ํ๋ API๋ฅผ ์ฝ๊ฒ ์ฐพ๊ณ ๊ด๋ฆฌํ ์ ์์ต๋๋ค. Swagger์์ API๋ฅผ ๊ทธ๋ฃนํํ๋ ๋ฐฉ๋ฒ์ ์ด๋ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ ์๋๋์ ๋ฐ๋ผ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ํ์ธํด์ ์ ์ฉํ๋ฉด ๋ฉ๋๋ค.
springfox-swagger2๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket appApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("APP API")
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.app"))
.paths(PathSelectors.any())
.build();
}
@Bean
public Docket totalApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("TOTAL API")
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}
springdoc-openapi๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
@Configuration
public class SwaggerConfig {
@Bean
public GroupedOpenApi appApi() {
return GroupedOpenApi.builder()
.group("APP API")
.pathsToMatch("/app/**")
.build();
}
@Bean
public GroupedOpenApi totalApi() {
return GroupedOpenApi.builder()
.group("TOTAL API")
.pathsToMatch("/**")
.build();
}
}
2. API ๋ฒ์ ๊ด๋ฆฌ ์ @Deprecated ํ์ฉํ๊ธฐ
ํ ๋ด์ ๋ฒ์ ๊ด๋ฆฌ ์ ๋ต์ ๋ฐ๋ผ ๋ค๋ฅด๊ฒ ์ง๋ง API ๋ฒ์ ์ ๊ด๋ฆฌํ ๋ ์ผ๋ฐ์ ์ผ๋ก @Deprecated ์ด๋ ธํ ์ด์ ์ ํ์ฉํ๋ ๊ฒ์ ๊ฝค๋ ์ ์ฉํฉ๋๋ค.
@Tag(name = "[APP] ํ
์คํธ")
@RestController
@RequestMapping("/app")
public class TestController {
@Deprecated
@GetMapping(Version.V1 + "/test")
public void testV1() {
}
@GetMapping(Version.V2 + "/test")
public void testV2() {
}
}
class Version {
public static final String V1 = "/v1";
public static final String V2 = "/v2";
}
์์ ์์ ์์๋ /v1/app/test ์๋ํฌ์ธํธ๊ฐ ์ด์ /v2/app/test๋ก ๋ณ๊ฒฝ๋์ด์ผ ํ๋ค๋ ๊ฐ์ ํ์ V1 ๋ฒ์ ์ API์ @Deprecated ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํฉ๋๋ค. ์ด๋ฅผ ํตํด Swagger UI์์์๋ ๋ค์๊ณผ ๊ฐ์ด ํ์๋ฉ๋๋ค.
@Deprecated ์ด๋ ธํ ์ด์ ์ด ๋ถ์ ์๋ํฌ์ธํธ๋ ๋ธ๋ผ์ธ๋ ์ฒ๋ฆฌ๋์ง๋ง, ์ฌ์ ํ Swagger ์์์ API ํ ์คํธ๋ ๊ฐ๋ฅํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ช ์ธ๋ฅผ ์ ๋ฌ๋ฐ์ ๊ฐ๋ฐ์์๊ฒ V1์ ๊ณง ์ฌ๋ผ์ง ๊ฒ์ด๋ผ๋ ๊ฒ์ ์๋ฆฌ๊ณ , ๋ ์ด์ ์ฌ์ฉํ์ง ์๋๋ก ์ ๋ํ ์ ์์ต๋๋ค.
@Deprecated ์ด๋ ธํ ์ด์ ์ ์์ฑ
์ถ๊ฐ๋ก @Deprecated ์ด๋ ธํ ์ด์ ์๋ since์ forRemoval์ด๋ผ๋ ์ต์ ์ด ์กด์ฌํฉ๋๋ค.
@Deprecated(since = "v1.1.0", forRemoval = true)
@GetMapping(Version.V1 + "/test")
public void testV1() {
}
์ด ์ต์ ๋ค์ ์ฝ๋์ ๋์์ ์ง์ ์ ์ผ๋ก ์ํฅ์ ๋ฏธ์น์ง๋ ์์ง๋ง, ์ด ์ต์ ๋ค์ ํตํด ์ฝ๋๋ฅผ ๋ณด๋ ๊ฐ๋ฐ์๋ค์๊ฒ ์ ์ฉํ ์ ๋ณด๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค. ์ผ๋ฐ์ ์ผ๋ก since์๋ @Deprecated๊ฐ ๋ถ์ฌ์ง๊ฒ ๋ ์คํ๋ฆฐํธ ํน์ ์ ํ์ ๋ฒ์ ์ ๋ช ์ํ๊ณ , forRemoval์ด true์ธ ๊ฒฝ์ฐ์๋ ํด๋น API๊ฐ ๋ค์ ๋ฒ์ ์์ ๊ณง๋ฐ๋ก ์ ๊ฑฐ๋ ์ ์์์ ์๋ฏธํฉ๋๋ค.
๋ณดํต forRemoval๊ฐ false์ผ ๊ฒฝ์ฐ์๋ @Deprecated๋ฅผ ๋ถ์ธ ๊ฐ๋ฐ์๊ฐ ๋ค์ ๋ฒ์ ์์ ์ง์ ์ฝ๋๋ฅผ ์ ๊ฑฐํ๊ฑฐ๋ ํด๋น ํ์คํ ๋ฆฌ๋ฅผ ์๋ ๊ฐ๋ฐ์๊ฐ ์ญ์ ํฉ๋๋ค. true์ผ ๊ฒฝ์ฐ์๋ '์ด ์ฝ๋๋ฅผ ์ง์๋ ๋ ๊น?'๋ผ๋ ๊ฑฑ์ ์์ด ํด๋น ์ฝ๋๋ฅผ ๋ณธ ๋๊ตฌ๋ ์์ฌํ๊ณ ์ ๊ฑฐํ ์ ์๋ค๋ ๊ฒ์ ์๋ฏธํ๋ฉฐ ์ด๋ฅผ ํตํด ๋ ๊ฑฐ์๊ฐ ๋ ์ฝ๋๋ฅผ ๊ณง๋ฐ๋ก ์ ๊ฑฐํ๋๋ก ์ ๋ํ ์ ์์ต๋๋ค.
3. ๋ช ์ธ๋ง ๋จผ์ ์ ๋ฌํ๊ธฐ
API ๋ก์ง์ด ์ ๋ถ ์์ฑ๋ ํ ๋ช ์ธ๋ฅผ ์ ๋ฌํ๋ฉด ํ๋ก ํธ ๊ฐ๋ฐ์๋ค์ด ๊ธฐ๋ค๋ฆฌ๋ ์๊ฐ์ด ๊ธธ์ด์ ธ ๋ฆฌ์์ค๋ฅผ ๋ญ๋นํ๊ฒ ๋ฉ๋๋ค. ์ด๋ด ๋๋ ์๋์ ๊ฐ์ด ๋น ๊ฐ์ฒด๋ง ๋จผ์ ์ ๋ฌํ์ฌ ์ด๋ค Response๋ฅผ ๋๊ฒจ์ค์ง ๋ช ์ธ๋ง ๋จผ์ ์ ๊ณตํ๊ณ , ์ดํ์ ๋ก์ง์ ๊ฐ๋ฐํ๋ ๋ฐฉ์์ผ๋ก ์ข ๋ ์ํํ๊ฒ ํ์ ์ ์งํํ ์ ์์ต๋๋ค.
@GetMapping("/test")
public ResponseDto test() {
// TODO ๋น์ฆ๋์ค ๋ก์ง ์์ฑ
return new ResponseDto();
}
์ด๋ ๊ฒ ํ๋ฉด ํ๋ก ํธ ๊ฐ๋ฐ์๋ ๋ช ์ธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ ์ ์์ํ ์ ์๊ณ , ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ ๋ก์ง์ ์ฐจํ์ ๊ตฌํํ ์ ์์ด ๋ ๋ค๋ฅธ ๋ช ์ธ๋ฅผ ์์ ํ๊ฑฐ๋ ๋ค๋ฅธ ์์ ์ ๋จผ์ ์งํํ๋ ๋ฑ ์์ชฝ ๋ชจ๋ ์์ ์ ๋ณํํ์ฌ ํจ์จ์ ์ผ๋ก ์งํํ ์ ์๊ฒ ๋ฉ๋๋ค.
4. Authorize์ jwt ๋ฃ์ ๋ prefix์ Bearer ์๋ต์ํค๊ธฐ
Swagger UI์์ Authorization์ JWT ํ ํฐ์ ๋ฃ์ ๋๋ 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~'์ ๊ฐ์ด 'Bearer '๋ฅผ prefix์ ์๋์ผ๋ก ๋ช ์ํด์ผ๋ง ํฉ๋๋ค. ์ด๋ฐ ๋ฒ๊ฑฐ๋ก์ด ๊ณผ์ ์ Swagger ์ค์ ์ ํตํด ์๋ตํ ์ ์์ต๋๋ค.
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components()
.addSecuritySchemes("Authorization",
new SecurityScheme().type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.description("JWT ํ ํฐ ์ ๋ณด")
)
);
}
์์ ๊ฐ์ด SecurityScheme ์ค์ ์ ํตํด ์ฌ์ฉ์๊ฐ Swagger ์์์ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~' ํ์์ผ๋ก๋ง ์ ๋ ฅํด๋ ์๋์ผ๋ก 'Bearer '๊ฐ prefix์ ์ถ๊ฐ๋๋๋ก ํ ์ ์์ต๋๋ค. ์ค์ ํ ์ค์ ๋ก Swagger์์ API๋ฅผ ์์ฒญํ ๋ค curl ๋ช ๋ น์ด๋ฅผ ํ์ธํด ๋ณด๋ฉด -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9~"์ ๊ฐ์ด Bearer๊ฐ ์๋์ผ๋ก ๋ถ๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
5. ๋ธ๋ผ์ฐ์ ์๋ก๊ณ ์นจ ํ์๋ ์ธ์ฆ์ ๋ณด ์ ์ง์ํค๊ธฐ
Swagger๋ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ๋ธ๋ผ์ฐ์ ๋ฅผ ์ข ๋ฃํ ํ ๋ค์ ์ด๋ฉด ์ธ์ฆ ์ ๋ณด๊ฐ ์ด๊ธฐํ๋์ด ๋งค๋ฒ ํ ํฐ์ ๋ค์ ์ ๋ ฅํด์ผ ํ๋ ๋ฒ๊ฑฐ๋ก์์ด ์์ต๋๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด 2020๋ 3์ 29์ผ PR #5939์ด ์ฌ๋ผ์๋๋ฐ, ์ด PR ๋๋ถ์ ๊ฐ๋จํ ์ค์ ์ถ๊ฐ๋ง์ผ๋ก ์ธ์ฆ ์ ๋ณด๋ฅผ ์ ์ง์ํฌ ์ ์๊ฒ ๋์์ต๋๋ค. ์ค์ ๋ฐฉ๋ฒ์ ๊ฐ๋จํ๋ฐ yml์ persist-authorization์ true๋ก ์ค์ ํ๋ฉด ๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ก๊ณ ์นจํ๊ฑฐ๋ ์ฌ์์ํด๋ ์ธ์ฆ ์ ๋ณด๊ฐ ์ ์ง๋ฉ๋๋ค.
#springdoc swagger
springdoc:
swagger-ui:
persist-authorization: true # ๋ธ๋ผ์ฐ์ ๋ฅผ ์๋ก๊ณ ์นจ ํ๋๋ผ๋ ์ธ์ฆ์ ๋ณด ์ ์ง
๋ ์์ธํ ๊ด๋ จ ์ ๋ณด๋ configuration.md์์ ํ์ธ ๊ฐ๋ฅํฉ๋๋ค.
6. ๊ธฐํ ์์ํ ์์ธ ์ค์ ๋ค
์ฝ๋์ Swagger ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ API์ ๋ํด ๋ ์์ธํ ์ ๋ณด๋ฅผ ์ ๊ณตํ ์ ์์ต๋๋ค. ํ์ง๋ง ์ด ๋ถ๋ถ์ ์ด์ ์ฝ๋์ Swagger ์ด๋ ธํ ์ด์ ์ด ๋ง์ด ์นจํฌ๋๋ค๋ ์ ๋๋ฌธ์ Swagger์ ๋จ์ ์ผ๋ก ๋ถ๋ฆฌ๊ธฐ๋ ํฉ๋๋ค. ๋ฐ๋ผ์ ํญ์ ๋ชจ๋ ์ด๋ ธํ ์ด์ ์ ์ ์ฉํ๊ธฐ๋ณด๋ค๋ ๊ฐ API ๋ณ๋ก ํ์ํ ๋ถ๋ถ๋ง ์ ํ์ ์ผ๋ก ์ ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํฉ๋๋ค.(์ ๋ ๋ณดํต @Tag, @Schema, @Operation๊น์ง๋ง ์ ์ฉํ๊ณ ํ์์ ๋ฐ๋ผ @ApiResponse๊น์ง ์ฌ์ฉํ๋ ๊ฒ ๊ฐ์ต๋๋ค.)
- @Tag: API ๊ทธ๋ฃน ์ ์
- @Schema: ๋ฐ์ดํฐ ๋ชจ๋ธ์ ์คํค๋ง ์ ์
- @Operation: ํน์ ์๋ํฌ์ธํธ์ ์์ ์ค๋ช
- @ApiResponse: ํน์ ์๋ํฌ์ธํธ์ ์๋ต ์ค๋ช
- @Parameter ๋ฉ์๋ ๋งค๊ฐ๋ณ์ ์ค๋ช
์ถ๊ฐ๋ก ํ๋กํผํฐ ์ค์ ์ ํตํด Swagger ๋ด API๋ค์ ์ ๋ ฌ ์์, ๊ธฐ๋ณธ์ผ๋ก ์ ๋ถ ํผ์น๊ธฐ/์ ๊ธฐ ๋ฑ์ ์ค์ ์ ํตํด ๊ฐ๋ ์ฑ๋ ํฅ์์ํฌ ์ ์์ผ๋ ์ด ๋ถ๋ถ๋ ์ํฉ์ ๋ง๊ฒ ์ ์ฉํ๋ฉด ๋์์ด ๋ฉ๋๋ค.