Springdoc OpenAPI UI๋Š” OpenAPI 3 ๊ธฐ๋ฐ˜์˜ Swagger API ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ œ๊ณตํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ด๋‹ค. ์˜ค๋Š˜์€ Springdoc OpenAPI UI ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด์„œ ํ•™์Šตํ•ด๋ณด๋ฉด์„œ ์•Œ๊ฒŒ๋œ ์œ ์šฉํ•œ ์ •๋ณด๋“ค ๋Œ€ํ•ด์„œ ๊ณต์œ ํ•ด๋ณด๊ณ ์ž ํ•œ๋‹ค.

Select a definition ๊ธฐ๋ณธ ์„ ํƒํ•˜๊ธฐ

GroupeOpenAPI๋ฅผ ๋“ฑ๋กํ•˜์—ฌ ํ•˜๋‚˜๊ฐ€ ์•„๋‹Œ ๋‹ค์ˆ˜์˜ API ๊ทธ๋ฃน์„ ์ •์˜ํ•˜๊ณ ์ž ํ• ๋•Œ springdoc.swagger-ui.urls-primary-name์— ๊ธฐ๋ณธ์œผ๋กœ ์„ ํƒํ•˜๊ณ  ์‹ถ์€ ๊ทธ๋ฃน ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜๋ฉด ๊ธฐ๋ณธ์œผ๋กœ ์„ ํƒ๋˜์–ด์ง„๋‹ค. ๊ทธ๋ฃน ํ‘œ์‹œ ์ˆœ์„œ ์ •๋ ฌ์— ์˜ํ•ด ๊ธฐ๋ณธ๊ฐ’์ด ๋จผ์ € ๋…ธ์ถœ๋˜์ง€ ์•Š์„ ๋•Œ ์œ ์šฉํ•˜๋‹ค.

springdoc:
  swagger-ui:
    path: /swagger-ui.html
    groups-order: desc
    urls-primary-name: v1

์„œ๋ฒ„ URL ์ง์ ‘ ๊ด€๋ฆฌํ•˜๊ธฐ

๊ธฐ๋ณธ์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” URL๋กœ๋„ ์ถฉ๋ถ„ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ API ๋ฌธ์„œ์— ๋Œ€ํ•œ ์„œ๋ฒ„๋ฅผ ๋ณ„๋„๋กœ ์ œ๊ณตํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ์—”๋“œํฌ์ธํŠธ๋ฅผ ๋…ธ์ถœํ•ด์ค„ ํ•„์š”๊ฐ€ ์žˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝ์šฐ ํ•ด๋‹น ๊ธฐ๋Šฅ์€ ์ œ๊ณตํ•ด์ฃผ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋ฒ„ ์ฃผ์†Œ์— ๋Œ€ํ•œ ConfigurationProperties๋ฅผ ๋งŒ๋“ค๊ณ  ์ฃผ์ž…ํ•˜๋ฉด ๋œ๋‹ค.

springdoc:
  server:
    - url: /
      description: Default
@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "springdoc")
public class SpringdocServersProperties {
    private final List<Server> servers;
}
@Bean
public OpenAPI openAPI(SpringdocServersProperties serversProperties) {
    return new OpenAPI().servers(serversProperties.getServers());
}

๊ธฐ๋ณธ ์‘๋‹ต ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ๋น„ํ™œ์„ฑํ™”

์‹ค๋ฌด์—์„œ๋Š” GlobalOpenApiCustomizer ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ์‘๋‹ต ๋ฉ”์‹œ์ง€ ์ค‘์—์„œ ์ผ๋ถ€์— ๋Œ€ํ•ด์„œ๋งŒ ํ‘œ์‹œ๋˜๋„๋ก ํ–ˆ๋Š”๋ฐ ์Šคํƒ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ์˜ ๋‹ต๋ณ€์„ ๋ณด๋‹ˆ ๊ธฐ๋ณธ์ ์œผ๋กœ @ControllerAdvice์— ์˜ํ•ด ์ฐพ์•„์ฃผ์–ด ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ์‘๋‹ต ๋ฉ”์‹œ์ง€๋ฅผ ์ œ์™ธํ•  ์ˆ˜ ์žˆ๋‹ค.

springdoc:
  override-with-generic-response: false
@Bean
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
    return openapi -> openapi.getPaths().values()
        .forEach(pathItem -> pathItem.readOperations()
            .forEach(operation -> operation.getResponses()
            .addApiResponse("500", new ApiResponse().description("Server Error")
                .content(new Content().addMediaType("application/json", new MediaType())))));
}

Could not resolve pointer: /components/schemas/XXX does not exist in document

API ๊ทธ๋ฃน๋งˆ๋‹ค ๋‹ค๋ฅธ ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ ๋ถ„๋ฆฌํ•œ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด .components ๋ฅผ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ์ฃผ์˜ํ•˜๋„๋ก ํ•ด์•ผํ•œ๋‹ค. ์ธ์ฆ ์Šคํ‚ค๋งˆ ์ด์™ธ์— ์š”์ฒญ๊ณผ ์‘๋‹ต์— ๋Œ€ํ•œ ์Šคํ‚ค๋งˆ ํด๋ž˜์Šค๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— .getComponents ๋ฅผ ์ด์šฉํ•˜์—ฌ ์ธ์ฆ ์Šคํ‚ค๋งˆ๋ฅผ ์ถ”๊ฐ€ํ•˜์ž.

@Bean
public GroupedOpenApi apiV1() {
    SecurityScheme bearerScheme = new SecurityScheme()
        .type(SecurityScheme.Type.HTTP)
        .in(SecurityScheme.In.HEADER)
        .scheme("bearer")
        .bearerFormat("JWT");

    return GroupedOpenApi.builder()
        .group("v1")
        .pathsToMatch("/api/**")
        .addOpenApiCustomiser(openapi -> openapi.info(info())
            .addSecurityItem(new SecurityRequirement().addList(DEFAULT_AUTH))
            //.components(new Components().addSecuritySchemes(DEFAULT_AUTH, bearerScheme)) // Resolve error!
            .getComponents().addSecuritySchemes(DEFAULT_AUTH, bearerScheme)
        )
        .build();
}

์ด์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์ฝ”๋“œ๋Š” spring-boot2-springdoc ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.