본문 바로가기
FrameWork/Spring

[Spring] OpenApi 3.0 Swagger Springdoc 적용

by 계범 2022. 12. 4.

Swagger란?

Swagger 는 REST API를 설계, 빌드, 문서화 및 사용하는 데 도움이 될 수 있는

OpenAPI Specification(REST API에 대한 API 설명 형식)을 기반으로 구축된 오픈 소스.

문서화 및 간단한 테스트를 제공한다.

대표적인 라이브러리로 springfox , springdoc 이 있는데, springdoc을 사용할 예정이다.

 

springdoc 선택 이유

더보기

springfox는 업데이트가 잘되고 있지 않음. 2020 7월 3.0.0 버전이 마지막 업데이트.

springdoc은 2019년 7월에 처음 나와서 지속된 업데이트 중.

springdoc은 webflux 지원 및 더 발전되고 사용하기 쉬움.

springdoc은 그룹 간, api 간 정렬도 가능하고, 어노테이션의 사용범위가 달라짐에 의한, 중복 코드 양 줄임.(@Tag)

 

스프링부트 Swagger Config 설정

Gradle & yml 설정

더보기

build.gradle

// Swagger
    implementation 'org.springdoc:springdoc-openapi-ui:1.6.3'

yml 파일 설정

springdoc:
  api-docs:
    enabled: true
  swagger-ui:
    path: /swagger-ui
    disable-swagger-default-url: true
    display-request-duration: true
    tags-sorter: alpha
    operations-sorter: alpha
    doc-expansion: none
    syntax-highlight:
      theme: nord
    urls-primary-name: TEST API
    persist-authorization: true
    query-config-enabled: true
  pre-loading-enabled: true
  • api-docs.enabled: swagger 사용 여부 설정. default: true
  • path : 스웨거 접속 path 설정
  • disable-swagger-default-url : swagger-default-url인 petstore html 문서 비활성화 여부
  • display-request-duration : 스웨거에서 try it out 했을 시 request duration(요청 소요 시간) 표기
  • tags-sorter : 태그 정렬 기준
  • operations-sorter : 태그 내 각 api의 정렬 기준
    • 정렬 기준은 기본 값은 컨트롤러 내 정의한 api 메서드 순으로 됨.
    • alpha(알파벳 오름차순), method(http method)
  • doc-expansion :tag와 operation을 펼치는 방식 설정
    • “list”, “full”, “none” 으로 설정 가능
    • “none” 설정 시 모두 닫힌 상태로 문서 열람
  • syntax-highlight : 구문 강조 표시 활성화
    • theme : 구문 색상 변경
      • "agate", "arta", "monokai", "nord", "obsidian", "tomorrow-night"
      • 예시 json 색상 변경이라고 생각하면 됨
  • urls-primary-name : 스웨거 ui 로드 때 표기되는 스웨거 그룹의 이름 표기
  • persist-authorization : true로 설정하면 권한 부여 데이터가 유지되고 브라우저 닫기/새로 고침에서 손실되지 않음.
  • query-config-enabled : 현재 설정되어있는 config들을 적용시키고, 초기 접근 시 셋팅 및 재정의 가능.
  • pre-loading-enabled : 프로그램 시작 시 Open API을 불러오기 위한 사전 셋팅

기본 셋팅

더보기
@Bean
public OpenAPI getOpenApi() {
  Server server = new Server().url("/");
  
  return new OpenAPI()
          .info(getApiInfo())
          .components(getComponents())
          .addServersItem(server);
}
  • OpenApi를 통해 기본 설정을 함
    • Info : 해당 OpenApi에 대한 정보 설정
    • Components : 구성요소 설정
    • serversItem : 서버 설정. 스웨거 테스트 시 http → https로 요청하기 위해 “/”로 설정하였다. ( 서버 url을 따라가게 됨)
private Info getApiInfo() {
    return new Info()
            .title(TITLE)
            .description(DESCRIPTION)
            .version(VERSION)
            .license(new License().name(LICENSE).url(LICENSE_URL));
}
private Components getComponents() {
    return new Components().addSecuritySchemes(JWT, getJwtSecurityScheme());}
  • addSecuritySchemes는 추후 설명. Autorization Header 추가하기 위함.

 Api 묶음 처리

더보기
@Bean
public GroupedOpenApi testGroupApi() {
    String[] paths = {ApiPath.TEST_GROUP_PATH1, ApiPath.TEST_GROUP_PATH2};    
    
    return GroupedOpenApi.builder()
            .group(TEST_GROUP_NAME)
            .pathsToMatch(paths)
            .addOpenApiCustomiser(openApiCustomiser())
            .build();
}
  • GroupedOpenApi를 통해 API를 묶음처리하여 표시할 수 있다
    • group(String) : 그룹 명칭
    • pathsToMatch(String…) : 매칭할 Path경로들.
    • addOpenApiCustomiser(OpenApiCustomiser) : 커스텀마이징 기능

Authorization Header 추가하기(인증정보)

더보기

위의 OpenApi 셋팅 때,

components에 addSecuritySchemes(JWT, getJwtSecurityScheme()) 구성요소로 추가하고,

addSecurityItem(securityItem) 을 통해 설정

@Bean
public OpenAPI getOpenApi() {
    Components components = new Components()
            .addSecuritySchemes(JWT, getJwtSecurityScheme());
    SecurityRequirement securityItem = new SecurityRequirement()
            .addList(JWT);

    return new OpenAPI()
            .info(getApiInfo())
            .components(components)
            .addSecurityItem(securityItem);
}
private SecurityScheme getJwtSecurityScheme() {
    return new SecurityScheme()
            .type(SecurityScheme.Type.APIKEY)
            .in(SecurityScheme.In.HEADER)
            .name(TOKEN);}

각 api 별 header 값 추가 및 커스텀마이징

더보기

OpenApiCustomiser 를 통해 커스텀마이징 가능

@Bean
public OpenApiCustomiser openApiCustomiser() {

    Map<String, Example> tenantDomainMapBySwagger = aopManager.getTenantDomainMapBySwagger();    
    
    return OpenApi -> OpenApi
            .addSecurityItem(getSecurityItem())
            .addServersItem(getServersItem())
            .getPaths().values().stream()
            .flatMap(pathItem -> pathItem.readOperations().stream())
            .forEach(operation -> operation
                    .addParametersItem(new HeaderParameter()
                            .name(TENANT_DOMAIN_HEADER)
                            .examples(tenantDomainMapBySwagger)
                            .required(Boolean.TRUE)
                            .schema(createTenantStringSchema()
                    )
            );
}

OpenAPi 내 paths 들을 돌아서 각 path별로
.addParametersItem()을 통해 header값을 추가시켰다.

안에선 환경별 default를 지정하여, 자동으로 header값으로 들어가게 햇고
select box 내에서 선택 가능하게 Examples 데이터도 넣어놨다.

new HeaderParameter()
.name(TENANT_DOMAIN_HEADER)
.examples(tenantDomainMapBySwagger)
.required(Boolean.TRUE)
.schema(createTenantStringSchema())
private Schema createTenantStringSchema() {
    String default_tenant_domain = "";    
    if (EnvironmentUtils.isLocal(environment) || EnvironmentUtils.isDevelop(environment)) {
        default_tenant_domain = DEFAULT_DV_DOMAIN;    
        } else if (EnvironmentUtils.isQA(environment)) {
        default_tenant_domain = DEFAULT_QA_DOMAIN;    
        } else if (EnvironmentUtils.isStaging(environment)) {
        default_tenant_domain = DEFAULT_ST_DOMAIN;    
    }

    return new StringSchema()._default(default_tenant_domain);
}

전체 swagger config

더보기
package com.test.test.test.config.swagger;

import com.jainwon.acca.cms.aop.AopManager;
import com.jainwon.acca.core.util.EnvironmentUtils;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.examples.Example;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import io.swagger.v3.oas.models.parameters.HeaderParameter;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.GroupedOpenApi;
import org.springdoc.core.SpringDocUtils;
import org.springdoc.core.customizers.OpenApiCustomiser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.CookieValue;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;

import static com.test.test.test.config.swagger.SwaggerConfigConstants.*;
import static com.test.test.test.config.swagger.SwaggerGroupName.*;

@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {

    private final AopManager aopManager;
    private final ConfigurableEnvironment environment;

    static {
        SpringDocUtils.getConfig()
                .replaceWithClass(LocalDateTime.class, String.class)
                .replaceWithClass(LocalDate.class, String.class)
                .replaceWithClass(LocalTime.class, String.class)
                .addAnnotationsToIgnore(AuthenticationPrincipal.class, CookieValue.class);
    }

    @Bean
    public OpenAPI getOpenApi() {
        return new OpenAPI()
                .info(getApiInfo())
                .components(getComponents());
    }

    private Info getApiInfo() {
        return new Info()
                .title(TITLE)
                .description(DESCRIPTION)
                .version(VERSION)
                .license(new License().name(LICENSE).url(LICENSE_URL));
    }

    private Components getComponents() {
        return new Components().addSecuritySchemes(JWT, getJwtSecurityScheme());
    }

    private SecurityScheme getJwtSecurityScheme() {
        return new SecurityScheme()
                .type(SecurityScheme.Type.APIKEY)
                .in(SecurityScheme.In.HEADER)
                .name(TOKEN);
    }

    @Bean
    public OpenApiCustomiser openApiCustomiser() {

        Map<String, Example> tenantDomainMapBySwagger = aopManager.getTenantDomainMapBySwagger();

        return OpenApi -> OpenApi
                .addSecurityItem(getSecurityItem())
                .addServersItem(getServersItem())
                .getPaths().values().stream()
                .flatMap(pathItem -> pathItem.readOperations().stream())
                .forEach(operation -> operation
                        .addParametersItem(new HeaderParameter()
                                .name(TENANT_DOMAIN_HEADER)
                                .examples(tenantDomainMapBySwagger)
                                .required(Boolean.TRUE)
                                .schema(createTenantStringSchema()))
                        .addParametersItem(new HeaderParameter()
                                .name(TEST_HEADER)
                                .schema(new StringSchema()._default(TEST_HEADER_VALUE))
                                .required(Boolean.TRUE)
                                .description(TEST_HEADER_DESCRIPTION))
                );

    }

    private SecurityRequirement getSecurityItem() {
        return new SecurityRequirement().addList(JWT);
    }

    private Server getServersItem() {
        return new Server().url(SERVER_URL);
    }

    @Bean
    public GroupedOpenApi testGroupApi1() {
        String[] paths = {ApiPath.TEST_GROUP_PATH1, ApiPath.TEST_GROUP_PATH2};

        return GroupedOpenApi.builder()
                .group(TEST_GROUP_API1_NAME)
                .pathsToMatch(paths)
                .addOpenApiCustomiser(openApiCustomiser())
                .build();
    }

    @Bean
    public GroupedOpenApi testGroupApi2() {
        String[] paths = {ApiPath.TEST_GROUP_PATH3, ApiPath.TEST_GROUP_PATH4};

        return GroupedOpenApi.builder()
                .group(TEST_GROUP_API2_NAME)
                .pathsToMatch(paths)
                .addOpenApiCustomiser(openApiCustomiser())
                .build();
    }

    

    private Schema createTenantStringSchema() {
        String default_tenant_domain = "";

        if (EnvironmentUtils.isLocal(environment) || EnvironmentUtils.isDevelop(environment)) {
            default_tenant_domain = DEFAULT_DV_DOMAIN;
        } else if (EnvironmentUtils.isQA(environment)) {
            default_tenant_domain = DEFAULT_QA_DOMAIN;
        } else if (EnvironmentUtils.isStaging(environment)) {
            default_tenant_domain = DEFAULT_ST_DOMAIN;
        }

        return new StringSchema()._default(default_tenant_domain);
    }

}

연관 셋팅 처리(security) 및 환경별 적용

더보기

security 관련 추가

if (Boolean.FALSE.equals(EnvironmentUtils.isProduction(environment))) {
            web.ignoring().antMatchers("/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**");
        }

특정 리소스에 대한 진입 시 권한 요청을 무시 설정.

이를 통해 pr 환경을 제외한 환경에서만 접근할 수 있도록 설정.

yml단에서 환경별로 관리하여 springdoc.api-docs.enabled=false 설정으로 스웨거를 아예 꺼둘수도 있다.

 

MvcConfig에서 인터셉터 동작 제외 처리.

@Override
    public void addInterceptors(InterceptorRegistry registry) {

        List<String> excludePathPatterns = Arrays.asList(
                "/resources/**", "/static/**", "/webjars/**",
                "/health.html", "/robots.txt", "/error", "/expired", "/favicon.ico",
                "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**");

        registry.addInterceptor(contextInterceptor)
                .excludePathPatterns(excludePathPatterns);
    }

 

swagger 적용 어노테이션

Controller 단 설정

더보기

@Tag : 컨트롤러 적용 api 그룹 어노테이션

  • name : 이름
  • description : 상세 설명
 

@Operation : api 어노테이션

  • summary : 요약 설명
  • description : 상세 설명
  • responses : api response 리스트(미사용)
  • parameters: api parameter 리스트(미사용)

@ApiResponse : API 응답값

  • responseCode : 응답코드(“404”)
  • description : 상세 설명
  • content : 응답 코드에 대한 내용
    • @Schema : return 스키마 설정
      • implementation : 설정하고자하는 스키마 연결 
    • examples : 해당 예시 설정가능
      • @ExampleObject : 예시 객체
        • name : 이름
        • description : 설명
설정 리턴 스키마
예시 설정

@Parameter : path, cookie, query, header 등등 요청과 함께 들어오는 파라미터

  • name : 이름
  • description : 상세 설명
  • in : parameter로 들어올 타입
  • example : 예시
  • required : 해당 값 필수 여부

Model 단 설정

더보기

@Schema : Schma(Model) 어노테이션

  • description : 한글명
  • defaultValue : 기본값
  • allowableValues : 허용값
  • nullable : null 허용 여부
  • required : 해당 값 필수 여부
  • type : 타입
  • example : 예시
allowableValues에 지정해둔 것중에서만 선택 가능

 

  • @ModelAttribute의 경우, 별도로 @ParameterObject를 붙여줘야 뜸
    • 해당 Object 내의 필드에는 @Parameter를 붙여야 정상적으로 스웨거에서 동작

 

기타

swagger 2 -> 3 어노테이션 변경

더보기
  • @Api@Tag
  • @ApiIgnore@Parameter(hidden = true) or @Operation(hidden = true) or @Hidden
  • @ApiImplicitParam@Parameter
  • @ApiImplicitParams@Parameters
  • @ApiModel@Schema
  • @ApiModelProperty(hidden = true)@Schema(accessMode = READ_ONLY)
  • @ApiModelProperty@Schema
  • @ApiOperation(value = "foo", notes = "bar")@Operation(summary = "foo", description = "bar")
  • @ApiParam@Parameter
  • @ApiResponse(code = 404, message = "foo")@ApiResponse(responseCode = "404", description = "foo")

아직 해결하지 못한 부분들... ㅠㅠ

더보기
  • 환경별 설정
    • yml에서 swagger 사용 설정이 아닌, config 코드 단에서 설정하고 싶음..
    • 멀티 테넌시 구조에서 특정 테넌트만 swagger-ui 가 켜지게 설정하고 싶음(코드단에서 설정 가능하면 풀릴 것으로 보임)
  • 권한 설정
    • 모든 프로젝트에서 동일한 설정으로 권한처리를 하고 싶음
    • 현재는 AuthFilter 내에서 특정 header값을 통해 권한처리해둠
  • header 값 readonly 설정
    • OpenApiCustomiser 내에서 addParametersItem 를 통해 추가한 HeaderParameter()schema 에 readOnly 설정을 주었지만, 먹질 않음…

 

참조

https://springdoc.org/

https://github.com/OAI/OpenAPI-Specification/blob/3.0.1/versions/3.0.1.md

https://swagger.io/specification/

https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/

https://springdoc.org/faq.html#_how_can_i_aggregate_external_endpoints_exposing_openapi_3_spec_inside_one_single_application

댓글