๐Ÿ’ Spring

Springdoc์„ ์ด์šฉํ•œ API ๋ฌธ์„œ ์ž๋™ํ™” (Swagger, SpringBoot 3.x)

iseunghan 2023. 6. 8. 21:30
๋ฐ˜์‘ํ˜•

Spring Boot 3.x.x ๋ถ€ํ„ฐ๋Š” JavaEE → Jakarta EE๋กœ ๊ต์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ์ด์ „์— ์‚ฌ์šฉํ–ˆ๋˜ Springfox๋Š” ํ˜„์žฌ Spring Boot 3.x.x์— ๋Œ€ํ•œ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ด๋ค„์ง€์ง€ ์•Š๊ณ  ํ˜„์‹œ์ ์—๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค. Springdoc ๊ณต์‹๋ฌธ์„œ์—์„œ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ์ž์„ธํ•œ ๋ฐฉ๋ฒ•์ด ๋‚˜์™€์žˆ์œผ๋‹ˆ ์ฐธ๊ณ ํ•˜์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

springdoc-openapi v2.0.2

์˜์กด์„ฑ ์ถ”๊ฐ€

Spring Boot 3.x.x๋ถ€ํ„ฐ๋Š” ์•„๋ž˜ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด Swagger-ui ์„ค์ •์€ ๋์ž…๋‹ˆ๋‹ค.

implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2"
  • springdoc-openapi-ui → springdoc-openapi-starter-webmvc-ui ๋กœ ๊ต์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • springdoc-openapi-webflux-ui → springdoc-openapi-starter-webflux-ui ๋กœ ๊ต์ฒด๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๋‹น ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ /swagger-ui.html ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

์„ค์ • ํŒŒ์ผ

ํ˜„์žฌ API ๋ฌธ์„œ์˜ ์„ค๋ช…์ด๋‚˜, ๋ฒ„์ „ ๋“ฑ๋“ฑ ๋” ์„ธ๋ถ€์ ์œผ๋กœ ์„ค์ •ํ•ด์ค„ ์ˆœ ์—†์„๊นŒ์š”?

๋ฐ”๋กœ OpenApi๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class SpringdocConfig {

    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .addOpenApiCustomizer(openApi -> openApi
                        .info(new Info().title("TEST API DOCS")
                                .description("ํ…Œ์ŠคํŠธ API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")
                                .version("v1.0"))
                .group("Test Group")
                .packagesToScan("org.example.controller")
                .displayName("This is Test API")
                .build();
    }
}

application.yml ์„ค์ •

springdoc:
  packages-to-scan: org.example.controller
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    path: /docs/test.html
    tags-sorter: alpha
    operations-sorter: alpha
		syntax-highlight:
      activated: true
  api-docs:
    path: /v3/docs
    groups:
      enabled: true
  cache:
    disabled: true
  • springdoc.swagger-ui.tags-sorter: URI๋ฅผ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ
  • springdoc.swagger-ui.operations-sorter : HTTP ๋ฉ”์†Œ๋“œ๋ฅผ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ
    • alpha: ์•ŒํŒŒ๋ฒณ ์ˆœ์œผ๋กœ ์ •๋ ฌ
    • method: HTTP ๋ฉ”์†Œ๋“œ ์ˆœ์œผ๋กœ ์ •๋ ฌ

๋” ๋งŽ์€ properties ์„ค์ •์€ ๊ณต์‹๋ฌธ์„œ์—์„œ ํ™•์ธํ•˜์‹œ๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

Spring Security๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด?

implementation 'org.springdoc:springdoc-openapi-security:1.6.14'

์ด๋ฏธ springdoc-openapi-starter-webmvc-ui ์˜์กด์„ฑ์„ ์‚ฌ์šฉ์ค‘์ด๋ผ๋ฉด, ๋”ฐ๋กœ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์ค„ ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์ถ”๊ฐ€ํ•ด์คฌ๋˜ ์˜์กด์„ฑ์œผ๋กœ ์ธํ•ด @AuthenticationPrincipal ์–ด๋…ธํ…Œ์ด์…˜์€ ์ž๋™์œผ๋กœ swagger-ui์— ignore ๋˜๋ฏ€๋กœ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค.

Javadoc ์œผ๋กœ ๋ฌธ์„œ๋ฅผ ๊พธ๋ฉฐ๋ณด์ž

springdoc-openapi-starter-webmvc-ui ์˜์กด์„ฑ์„ ์‚ฌ์šฉ์ค‘์ด๋ผ๋ฉด, ์•„๋ž˜ ์˜์กด์„ฑ๋“ค์„ ์ถ”๊ฐ€ํ•ด์ค˜์„œ javadoc์„ ํ™œ์„ฑํ™” ์‹œ์ผœ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe:0.13.0'
implementation 'com.github.therapi:therapi-runtime-javadoc:0.13.0'

์ด์ œ ์ž‘์„ฑํ•œ javadoc์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.

  • method comment: @Operation(description={}) ์œผ๋กœ ์น˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
  • @return: @Operation(response={}) ์œผ๋กœ ์น˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
  • attribute comment: @Schema(description={}) ์œผ๋กœ ์น˜ํ™˜๋ฉ๋‹ˆ๋‹ค.
๐Ÿ’ก ๋งŒ์•ฝ Swagger Annotation๊ณผ javadoc์ด ๋™์‹œ์— ์กด์žฌํ•œ๋‹ค๋ฉด, Swagger Annotation์ด ์šฐ์„ ์œผ๋กœ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค.

Authorization ์ถ”๊ฐ€

JWT๋ฅผ ์‚ฌ์šฉํ•ด ์ธ๊ฐ€๋ฅผ ํ•˜๊ณ ์žˆ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •์„ ์ถ”๊ฐ€ํ•˜๋ฉด Swagger์—์„œ ์ „์—ญ ์ธ์ฆ์ •๋ณด๋ฅผ ์„ค์ •ํ•˜์—ฌ ์ธ์ฆ์ด ํ•„์š”ํ•œ API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Configuration
public class SpringdocConfig {

    @Bean
    public GroupedOpenApi publicApi() {
        return GroupedOpenApi.builder()
                .addSecurityItem(new SecurityRequirement().addList("Authorization"))
                .components(new Components().addSecuritySchemes("Authorization", new SecurityScheme()
                        .name("Authorization")
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")))
                .addOpenApiCustomizer(openApi -> openApi
                        .info(new Info().title("TEST API DOCS")
                                .description("ํ…Œ์ŠคํŠธ API ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค.")
                                .version("v1.0"))
                .group("Test Group")
                .packagesToScan("org.example.controller")
                .displayName("This is Test API")
                .build();
    }
}

๊ณตํ†ต Pageable ์‘๋‹ต ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐœ๋ฐœ

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Parameters({
        @Parameter(name = "page", description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0..N) [๊ธฐ๋ณธ๊ฐ’: 0]", schema = @Schema(type = "integer", defaultValue = "0", nullable = true)),
        @Parameter(name = "size", description = "ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0..100) [๊ธฐ๋ณธ๊ฐ’: 10]", schema = @Schema(type = "integer", defaultValue = "10")),
        @Parameter(name = "sort", description = "์ •๋ ฌ (์ปฌ๋Ÿผ,asc|desc) [์˜ˆ์‹œ] ์ด๋ฆ„์„ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ex) 'name,desc'", schema = @Schema(type = "array", name = "์ •๋ ฌ (์ปฌ๋Ÿผ,asc|desc) [์˜ˆ์‹œ] ์ด๋ฆ„์„ ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ ex)")),
        @Parameter(name = "pageable", hidden = true)
})
public @interface ApiPageable {
}

๊ณตํ†ต API ์‘๋‹ต ์–ด๋…ธํ…Œ์ด์…˜ ๊ฐœ๋ฐœ

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ApiResponse(responseCode = "401", description = "UnAuthorized", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example")))
@ApiResponse(responseCode = "403", description = "Forbidden", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class),mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example")))
@ApiResponse(responseCode = "500", description = "Internal Server Error", content = @Content(schema = @Schema(implementation = YourExample.class)))
public @interface ApiCommonResponse {
}

Controller ๋ช…์„ธ

GET ๋ฐฉ์‹

@Operation(
        summary = "ํšŒ์› ์กฐํšŒ API",
        description = "ํšŒ์›์„ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.",
        responses = {
                @ApiResponse(responseCode = "200", description = "Success", useReturnTypeSchema = true, headers = @Header(name = "Authorization", required = true), content = @Content(schema = @Schema(implementation = YourExample.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "example"))),
                @ApiResponse(responseCode = "404", description = "NotFound", useReturnTypeSchema = true, content = @Content(schema = @Schema(implementation = YourExample.class), examples = @ExampleObject(value = "example"))),})
@Parameters({
        @Parameter(name = "username", description = "์‚ฌ์šฉ์ž ์ด๋ฆ„", example = "john", required = true),
        @Parameter(name = "age", description = "์‚ฌ์šฉ์ž ๋‚˜์ด", example = "20", required = true)
})
@ApiPageable
@ApiCommonResponse
@GetMapping("/members")
ResponseEntity<MemberDto> searchMembers(
        @RequestParam("username") String username,
        @RequestParam("age") int age,
        @PageableDefault Pageable pageable
){
	...
}

POST ๋ฐฉ์‹

@Operation(summary = "ํšŒ์›์„ ์ƒ์„ฑํ•˜๋Š” API",
        requestBody = @RequestBody(description = "ํšŒ์› ์ƒ์„ฑ์„ ์œ„ํ•œ DTO"),
        responses = {
                @ApiResponse(responseCode = "200", description = "Success", useReturnTypeSchema = true, headers = @Header(name = "Authorization", required = true), content = @Content(schema = @Schema(implementation = CreateProjectResponse.class), mediaType = APPLICATION_JSON_VALUE, examples = @ExampleObject(value = "{\\"success\\":true,\\"error\\":null,\\"message\\":{\\"projectId\\":1}"))),
                ...
        })
@ApiCommonResponse
ResponseEntity<Long> createProject(
        @Valid @RequestBody CreateMemberDto request
) {
	...
}

 

 

REFERENCES

 

springdoc-openapi v2.1.0

springdoc-openapi java library helps to automate the generation of API documentation using spring boot projects. springdoc-openapi works by examining an application at runtime to infer API semantics based on spring configurations, class structure and vario

springdoc.org

 

Set JWT with Spring Boot and Swagger UI | Baeldung

Learn how to set a JSON Web Token on requests to Swagger UI running in Spring Boot.

www.baeldung.com

๋ฐ˜์‘ํ˜•