[SpringDoc] SpringFox -> SpringDoc 마이그레이션 일지
목차
0. 서론
1. Swagger vs SpringFox vs SpringDoc
2. 왜 SpringFox보다 SpringDoc일까?
3. SpringDoc으로 마이그레이션하기
4. 트러블슈팅
0. 서론
최근 팀 코드에 SpringFox를 SpringDoc으로 전환하는 작업을 진행하고 배포까지 무사히 마쳤다. 해당 글에서는 SpringDoc이란 무엇이고, 왜 SpringDoc 마이그레이션 하게 되었는지, 어떻게 마이그레이션 했고 그 과정 중 어떤 이슈가 있었는지 내용을 정리하려 한다.
1. Swagger vs SpringFox vs SpringDoc
🟢 Swagger
- API 설계, 테스트, 문서화 등을 편리하게 이용할 수 있게 돕는 프레임워크
- Swagger UI: OenAPI를 기반으로 시각적 문서를 제공하여 문서화 기능을 쉽게 제공
OpenAPI: HTTP API를 설명하기 위한 공식 표준을 제공
🟢 SpringFox & SpringDoc
- Spring을 이용해 API 문서 생성을 자동화하는 데 도움을 주는 라이브러리
내 이해를 바탕으로 비유를 해보자면 Swagger UI는 인터페이스고, SpringFox와 SpringDoc은 구현체이다. 사용자가 상호작용하는 시각적인 부분은 Swagger UI이 담당하고, 그 시각적인 부분을 채우는 메타데이터(API에 대한 정보나 응답값이 어떤 형태인지 등)를 가져오는 일은 SpringFox이나 SpringDoc이 담당한다.
2. 왜 SpringFox보다 SpringDoc일까?
최근에는 SpringFox보다 SpringDoc을 권장하는 글이 심심치 않게 보인다. 그 이유는 아래와 같다.
참고: https://springdoc.org/index.html#differentiation-to-springfox-project
- SpringFox는 2020년 7월 이후로 더 이상 업데이트되지 않는다. (mvn repository 기준)
- SpringFox는 OpenAPI 3을 지원하지 않는다.
- SpringFox는 Spring Boot 3 이상에서는 사용이 불가능하다.
단순히 SpringFox가 업데이트되지 않는다는 말로는 팀원들을 설득하기 힘들었기에 아래와 같은 사유를 덧붙였다.
- (다른 레포에 시범적으로 도입해보았는데) 설정이나 커스텀이 훨씬 간편하고 명시적이었다.
- 사용해야하는 어노테이션 종류가 달라질 뿐, 사용 방법은 기존과 유사하다.
- 모두가 원하지만 당장 실행하기는 힘든 Spring 버전업에서 SpringFox는 방해 요소가 될 수 있다. 리소스가 여유 있을 때 미리 SpringDoc 전환 작업을 진행하여 버전업을 위한 길을 닦아두고 싶다.
감사하게도 마이그레이션을 진행하는 쪽으로 결론이 나서, 본격적으로 SpringDoc 마이그레이션을 시작하게 되었다.
3. SpringDoc으로 마이그레이션하기
참고: https://springdoc.org/#migrating-from-springfox
0️⃣ 예제 버전
- Spring Boot v2.3.2
- SpringDoc v1.7.0
왜 SpringDoc v1.7.0을 선택했나요?
👉 Spring Boot 2.x, 1.x를 지원하는 버전 중 가장 최신 버전은 1.7.0이다. (참고: https://springdoc.org/)
1️⃣ build.gradle 변경
// AS-IS
implementation "io.springfox:springfox-swagger-ui:3.0.0"
implementation "io.springfox:springfox-boot-starter:3.0.0"
// TO-BE
implementation "org.springdoc:springdoc-openapi-ui:1.7.0"
참고로 Spring Boot 3부터는 'org.springdoc:springdoc-openapi-starter-webmvc-ui'도 필요하다고 한다.
2️⃣ 어노테이션 변경
SpringFox | 속성 | SpringDoc | 속성 | 설명 |
@Api | @Tag | 태그 | ||
tags | name | 태그의 이름 | ||
value | description | 설명 | ||
@ApiModelProperty | @Schema | 모델의 속성 | ||
required = true | requiredMode = REQUIRED | 필수 항목 | ||
required = false | requiredMode = NOT_REQUIRED | 필수가 아닌 항목 | ||
value | description | 필드 설명 | ||
notes | example | 필드 추가 설명 | ||
dataType | type | 타입 | ||
position | - (대체 없음. 필드 선언 순으로 나타남) | 순서 | ||
allowEmptyValue | nullable | null 가능 여부 | ||
@ApiModel | @Schema | 모델의 속성 | ||
@ApiOperation | @Operation | 메서드에 대한 추가 속성 정의 | ||
value | summary | 120자 이하의 간략한 요약, 설명 | ||
notes | description | 자세한 설명 | ||
@ApiParam | @Parameter | 매개변수(파라미터)에 대한 주석 | ||
value | description | 설명(ex: 파라미터의 목적) | ||
defaultValue | schema = @Schema(defaultValue = "") | 기본값 설정 | ||
@ApiIgnore | @Hidden 또는 @Parameter(hidden = true) 또는 @Operation(hiddne = true) |
숨김 처리 |
3️⃣ 메타데이터 변경
아래 이미지의 '테스트용 Swagger'와 같은 Swagger 제목이나 바로가기 링크 등을 설정한다.
- AS-IS (SpringFox)
- Docket의 apiInfo()를 통해 세팅
@Bean
public Docket api() {
var contact = new Contact("연로그", "https://yeonyeon.tistory.com", "yeonlog06@gmail.com");
var apiInfo = new ApiInfoBuilder().title("테스트용 Swagger")
.description("테스트용 Swagger 미리보기")
.contact(contact)
.build();
return new Docket(DocumentationType.OAS_30)
.select()
...
.apiInfo(apiInfo);
}
- TO-BE (SpringDoc)
- OpenApi의 info() 이용
@Bean
public OpenAPI metaData() {
var contact = new Contact()
.url("https://yeonyeon.tistory.com")
.email("yeonlog06@gmail.com")
.name("연로그");
var info = new Info().title("테스트용 Swagger")
.description("테스트용 Swagger 미리보기")
.contact(contact);
return new OpenAPI()
.info(info);
}
4️⃣ Docket -> GroupedOpenApi 변경
Swagger 설정을 위해 기본적으로 사용하는 클래스를 변경한다.
- AS-IS (SpringFox)
- Docket 이용
@Bean
public Docket testAdminApi() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.ant("/test/admin/**"))
.build()
.enable(swaggerEnabled)
.genericModelSubstitutes(ResponseEntity.class)
.useDefaultResponseMessages(true)
.groupName("test-admin-api");
}
- TO-BE (SpringDoc)
- GroupedOpenApi 이용
- 여러개의 Docket을 사용했다면 GroupedOpenApi, 하나의 Docket을 사용했다면 OpenApi를 이용
- SpringFox의 여러 설정 생략 가능
@Bean
public GroupedOpenApi testAdminApi() {
return GroupedOpenApi.builder()
.group("test-admin-api")
.pathsToMatch("/test/admin/**")
.build();
}
5️⃣ 공통 헤더 추가
모든 API에 공통적으로 사용하는 헤더를 추가한다.
- AS-IS (SpringFox)
- Docket의 globalRequestParameters() 사용
@Bean
public Docket testAdminApi() {
return new Docket(DocumentationType.OAS_30)
...
.globalRequestParameters(Lists.newArrayList(
buildParameter("token", "토큰", null),
buildParameter("ageGroup", "성인", "ADULT"),
buildParameter("ageGroup", "미성년자", "CHILD")
));
}
private RequestParameter buildParameter(String name, String description, String value) {
return new RequestParameterBuilder()
.name(name)
.in(ParameterType.HEADER)
.description(description)
.example(new ExampleBuilder().value(value).build())
.required(false)
.build();
}
- TO-BE (SpringDoc)
- GroupedOpenApi에 OpenApiCustomiser 추가
- 단일 값은 example, 선택 가능한 값들은 examples에 세팅
- 더 다양한 옵션 제공
@Bean
public OpenApiCustomiser testHeaderCustom() {
var ageGroup = Map.of(
"성인", toExample("ADULT"),
"미성년자", toExample("CHILD")
);
var parameters = List.of(
buildParameter("token", "토큰", null),
buildParameterValues("ageGroup", "나이", ageGroup)
);
return openApi -> {
var paths = openApi.getPaths().values();
for (var path : paths) {
var operations = path.readOperations();
for (var operation : operations) {
for (var parameter : parameters) {
operation.addParametersItem(parameter);
}
}
}
};
}
private Example toExample(String value) {
return new Example().value(value);
}
private Parameter buildParameter(String name, String description, String value) {
return new HeaderParameter()
.name(name)
.description(description)
.required(false)
.example(value);
}
private Parameter buildParameterValues(String name, String description, Map<String, Example> values) {
return new HeaderParameter()
.name(name)
.description(description)
.required(false)
.examples(values);
}
4. 트러블슈팅
1️⃣ Swagger가 안 뜨는 현상 - Failed to load remote configuration. 에러
Swagger가 위와 같이 'Failed to load remote configuration'을 띄우며 제대로 뜨지 않았다. 개발자 도구를 켜보니 404 Not Found가 뜨고 있다. 사실 이 이슈에 대한 원인으로는 정말 많은 가능성이 있다.
나 같은 경우에는 build.gradle에 SpringFox와 SpringDoc을 둘 다 의존성이 추가된 상황이었다. SpringFox 의존성을 제거하니 해결되었다.
2️⃣ Schema 접기, 순서 설정
별도의 설정을 하지 않았다면, swagger을 들어가면 모든 API 목록과 모든 Schema 목록이 노출될 것이다.
이 중에서도 Schemas 목록을 접기 상태로 만들고 싶다면 application.propererties (또는 yaml)에 설정을 추가해 주면 된다.
# Schemas 접기 상태
springdoc.swagger-ui.defaultModelsExpandDepth=0
# Schemas 아예 미노출
springdoc.swagger-ui.defaultModelsExpandDepth=-1
추가적으로 여러개의 Schemas가 있다면, 순서가 뒤죽박죽인 것처럼 보일 것이다. 이를 abc 순으로 정렬하고 싶다면 아래와 같은 설정을 추가해 준다.
springdoc.writer-with-order-by-keys=true
3️⃣ Parameter의 examples 순서 지정
'3. 마이그레이션 과정 - 5 공통 헤더 추가'에서 봤듯 인자로 받을 값을 리스트 형태로 노출할 수 있다.
이때, 노출되는 값은 기본적으로 put 한 순서에 따라 노출된다.
// case 1. examples을 이용한 경우, Map에 put한 순서대로 노출된다.
var examples = new LinkedHashMap<String, Example>();
examples.put("미성년자", toExample("CHILD"));
examples.put("성인", toExample("ADULT"));
return new Parameter()
.name(name)
.description(description)
.examples(values);
// case 2. addExample을 이용한 경우, add한 순서대로 노출된다.
var parameter = new Parameter()
.name(name)
.description(description);
parameter.addExample("미성년자", toExample("CHILD"));
parameter.addExample("성인", toExample("ADULT"));
return parameter;
헌데, application.properties에 아래와 같은 설정이 있는 경우에 위 순서가 무시되고 key 값을 기준으로 정렬된다.
springdoc.writer-with-order-by-keys=true
정렬 방식을 변경하는 것보다 앞에 1, 2를 추가하는 방식으로 key의 값을 변경하여 해결했다.
var parameter = new Parameter()
.name(name)
.description(description);
parameter.addExample("1. 성인", toExample("ADULT"));
parameter.addExample("2. 미성년자", toExample("CHILD"));
return parameter;
4️⃣ 날짜 및 시간 타입 변경
LocalDate, LocalTime 같은 필드가 String이 아닌 '{ hour: number, minute: number, ... }' 형태로 반환되었다. 이는 아래와 같은 코드로 해결하였다.
static {
SpringDocUtils config = SpringDocUtils.getConfig();
config.replaceWithClass(LocalDate.class, String.class);
config.replaceWithClass(LocalTime.class, String.class);
config.replaceWithClass(YearMonth.class, String.class);
}
5️⃣ Enum에 대한 이슈
- name() 값이 아닌 toString() 값이 노출되는 현상
- @JsonFormat(shape = OBJECT)를 무시하는 현상
- Error creating bean with name 'XXX' 에러
위 현상에 대해서는 다음 링크를 참고하길 바란다. 👉 https://yeonyeon.tistory.com/323
6️⃣ @ModelAttribute와 @ParameterObject
- @ModelAttribute로 받는 요청값의 경우 swagger에서 json 형태로 받는 현상
- @ParameterObject를 사용하는 경우 모든 필드를 String으로 인식하는 현상
위 현상에 대해서는 다음 링크를 참고하길 바란다. 👉 https://yeonyeon.tistory.com/324