본문 바로가기
Develop/Spring+JPA

[SpringDoc] SpringFox -> SpringDoc 마이그레이션 일지

by 연로그 2024. 1. 14.
반응형

목차

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의 여러 설정 생략 가능

Swagger 우측상단을 통해 그룹을 선택할 수 있다.

 

@Bean
public GroupedOpenApi testAdminApi() {
    return GroupedOpenApi.builder()
        .group("test-admin-api")
        .pathsToMatch("/test/admin/**")
        .build();
}

 

 

5️⃣ 공통 헤더 추가

모든 API에 공통적으로 사용하는 헤더를 추가한다.

SpringDoc 화면

 

  • 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가 뜨고 있다. 사실 이 이슈에 대한 원인으로는 정말 많은 가능성이 있다.

 

gpt 피셜 예상 원인들

 

나 같은 경우에는 build.gradle에 SpringFox와 SpringDoc을 둘 다 의존성이 추가된 상황이었다. SpringFox 의존성을 제거하니 해결되었다.

 

 

2️⃣ Schema 접기, 순서 설정

별도의 설정을 하지 않았다면, swagger을 들어가면 모든 API 목록과 모든 Schema 목록이 노출될 것이다.

Swagger 화면

 

 

이 중에서도 Schemas 목록을 접기 상태로 만들고 싶다면 application.propererties (또는 yaml)에 설정을 추가해 주면 된다.

# Schemas 접기 상태
springdoc.swagger-ui.defaultModelsExpandDepth=0

# Schemas 아예 미노출
springdoc.swagger-ui.defaultModelsExpandDepth=-1

springdoc.swagger-ui.defaultModelsExpandDepth=0

 

추가적으로 여러개의 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

 

[SpringDoc] 우리 enum이 달라졌어요

SpringFox에서 SpringDoc으로 마이그레이션 하던 와중, 몇 가지 이슈가 발생했다. (참고글: [SpringDoc] SpringFox -> SpringDoc 마이그레이션 일지) 그중에서도 enum과 관련된 이슈가 많았는데, 어떤 이슈가 발생

yeonyeon.tistory.com

 

 

6️⃣ @ModelAttribute와 @ParameterObject

  • @ModelAttribute로 받는 요청값의 경우 swagger에서 json 형태로 받는 현상
  • @ParameterObject를 사용하는 경우 모든 필드를 String으로 인식하는 현상

위 현상에 대해서는 다음 링크를 참고하길 바란다. 👉 https://yeonyeon.tistory.com/324

 

[SpringDoc] 쿼리 파라미터가 이상하다?

SpringFox에서 SpringDoc으로 마이그레이션하던 와중, 몇가지 이슈가 발생했다. (참고글: [SpringDoc] SpringFox -> SpringDoc 마이그레이션 일지) 몇가지 이슈들 중에서도 쿼리 파라미터와 관련된 이슈를 해당

yeonyeon.tistory.com

 

반응형