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 색상 변경이라고 생각하면 됨
-
- theme : 구문 색상 변경
- 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 : 설명
- @ExampleObject : 예시 객체
- @Schema : return 스키마 설정
@Parameter : path, cookie, query, header 등등 요청과 함께 들어오는 파라미터
- name : 이름
- description : 상세 설명
- in : parameter로 들어올 타입
- example : 예시
- required : 해당 값 필수 여부
Model 단 설정
@Schema : Schma(Model) 어노테이션
- description : 한글명
- defaultValue : 기본값
- allowableValues : 허용값
- nullable : null 허용 여부
- required : 해당 값 필수 여부
- type : 타입
- example : 예시
- @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://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/
'FrameWork > Spring' 카테고리의 다른 글
[Spring] spring boot 3.x & Java 21로 버전업! (0) | 2024.09.19 |
---|---|
[Java] Id 생성전략(UUID 선택 및 테스트) & spring JPA Bulk insert (0) | 2023.05.05 |
[Spring] JPA Auditing 사용법 (0) | 2022.08.11 |
[Spring] @Async 비동기 멀티스레드 사용법 (4) | 2022.06.25 |
댓글