SpringBoot - Swagger ui 3.0 연동하기

1. Swagger 3.0 세팅하기.

구글에 검색해서 나온 스웨거 세팅 관련 포스트는 대부분 2.X 버전대 이거나, 3.0 라이브러리를 받았음에도, 스웨거 세팅에서 SWAGGER2버전으로 낮추어 사용하는 글이 혼재하여 정리하고자 글을 작성한다.

우선 내가 적용하고자 하는 스프링 부트의 버전은 2.7.1 버전이며, 스웨거는 open api 3.0 OAS3 문서 버전을 만들고자 한다.

  • build.gradle
// https://mvnrepository.com/artifact/io.springfox/springfox-boot-starter
implementation group: 'io.springfox', name: 'springfox-boot-starter', version: '3.0.0'
// https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui
implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '3.0.0'
// https://mvnrepository.com/artifact/io.springfox/springfox-oas
implementation group: 'io.springfox', name: 'springfox-oas', version: '3.0.0'
 3가지 버전을 받아서 사용했을 , 정상 구동이 가능 했다. 보통은 springfox-boot-starter 넣으면 된다는 얘기가 많았는데,  경우에는 아래 2가지도 있어야 구동이 정상적으로 가능했다.
  • SwaggerConfig.java
package com.sample.swagger.config;

import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@EnableWebMvc
@Configuration
public class SwaggerConfig extends WebMvcConfigurationSupport {

    @Bean
    public Docket api(TypeResolver typeResolver) {
        return new Docket(DocumentationType.OAS_30) // 3.0 문서버전으로 세팅
                .useDefaultResponseMessages(true)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sample.swagger.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger 3.0 Api Sample")
                .description("This is Sample")
                .version("1.0")
                .build();
    }
}

위의 설정을 웹 설정에 추가한다. 참고로, MVC설정을 따로 해주지 않는 프로젝트라면 아래의 설정도 같이 추가해주어야 한다.

  • WebConfig.java
package com.sample.swagger.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/swagger-ui/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
                .resourceChain(false);
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/swagger-ui/")
                .setViewName("forward:/swagger-ui/index.html");
    }

}

위 설정이 없다면, 아무리 프로젝트를 돌려도 스웨거 파싱 페이지에 접근 할 수 가 없다. (리소스가 없다고 에러가 발생한다.)

2. @Schema 작성

Swagger 3.0 버전에서는 ApiModelProperty 대신 Schema 어노테이션으로 모델을 작성한다. 파라미터 / 실제 응답 데이터 / 응답 내 데이터 3가지 종류에 대해 예제를 제공할 생각이므로, 세개를 다 정의해야 한다. 예제 구조는 dataParam / dataRes / data 로 잡았으며, dataRes 내부에 List 가 존재하는 것으로 세팅하였다.

@Schema
  • name : 모델명
  • description : 해당 모델에 대한 설명
  • defaultValue : 기본 값(있다면)
  • allowableValues : 허용 값 범위(있다면)
  • example : 예제 데이터 (작성해야 스웨거 문서에 데이터가 일반 타입이 아닌 데이터가 있는 것으로 나타남)

  • dataParam.java
package com.sample.swagger.data;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "dataParam", description = "유저 조회 파라미터")
@Getter
@Setter
public class dataParam {
    @Schema(description = "요청 개수", defaultValue = "", allowableValues = {}, example = "1")
    private int num;
}
  • data.java
package com.sample.swagger.data;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "data", description = "유저 데이터")
@Getter
@Setter
public class data {
    @Schema(description = "유저 번호", defaultValue = "", allowableValues = {}, example = "1")
    private int userNo;
    @Schema(description = "유저 이름", defaultValue = "", allowableValues = {}, example = "user1")
    private String username;
}
  • dataRes.java
package com.sample.swagger.data;

import java.util.List;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "dataRes", description = "유저 조회 응답 데이터")
@Getter
@Setter
public class dataRes {
    @Schema(description = "유저 리스트", defaultValue = "", allowableValues = {}, example = "")
    private List<data> dataList;
}

그리고, 응답을 정의할 클래스도 작성하였다. 여기서 주목할 점은 글로벌 에러 처리 혹은 기타 에러 처리로 인해 리턴되는 데이터 형태는 스웨거에서 잡지 못한다는 것이다. 따라서, 별도로 추가하고 싶은 예외 케이스의 경우(예를 들어, 200 정상 리턴 외, 400-500대 등의 에러를 스웨거에서 제공하고 싶은 경우.) 별도 클래스를 작성하여 @Schema 어노테이션을 준 후, 응답에 추가해주어야 한다.

우선, 응답 클래스 3가지를 작성한다.

  • ApiRes : 정상 응답 시, 컨트롤러에서 리턴하는 클래스
package com.sample.swagger.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "ApiRes", description = "Api Response Format")
@Getter
@Setter
public class ApiRes<T> {
    @Schema(description = "요청결과 상태", nullable = false, example = "success")
    private String result_status;
    @Schema(description = "요청결과", nullable = false, example = "")
    private T result;

    public ApiRes(T result) {
        this.result_status = "success";
        this.result = result;
    }

}
  • ApiErr : 에러가 발생한 경우 보여줄 에러
package com.sample.swagger.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "ApiErr", description = "Api Response Err")
@Getter
@Setter
public class ApiErr {
    @Schema(description = "요청결과 상태", nullable = false, example = "fail")
    private String error_status;
    @Schema(description = "요청결과 메세지", nullable = false, example = "요청이 실패하였습니다.")
    private String error_message;

    public ApiErr(String error_status, String error_message) {
        this.error_status = error_status;
        this.error_message = error_message;
    }
}
  • ApiNoAuth : 권한 없는 경우 보여줄 에러
package com.sample.swagger.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Schema(name = "ApiNoAuth", description = "Api Response No Auth")
@Getter
@Setter
public class ApiNoAuth {
    @Schema(description = "요청결과 상태", nullable = false, example = "fail")
    private String error_status;
    @Schema(description = "요청결과 메세지", nullable = false, example = "권한이 없습니다.")
    private String error_message;
}

데이터 및 응답에 대한 스키마를 작성하였으면, 해당 컨트롤러로 이동하여 컨트롤러에 대해 정의한다.

3. @ApiResponse, @Tag, @ApiOperation

@Tag : 같은 태그로 작성된 컨트롤러들을 묶어주는 역할을 한다.
  • name : 태그명 작성
  • description : 태그에 대한 설명
@ApiResponse : 해당 컨트롤러에 대한 응답을 정의한다.
  • 상위에 @ApiResponses(values={}) 로 묶인다.
  • responseCode : 응답코드를 정의한다.
  • description : 해당 응답에 대한 설명을 정의한다.
  • content : 해당 응답에 대한 내용을 정의한다. @Content를 통해 내용을 따로 정의할 수 있으며, 보통 @Schema implemention을 통해 미리 정의된 클래스 스키마를 사용 가능하다.
@ApiOperation: API 동작에 대한 설명을 정의한다.
  • value : 동작 값을 정의한다.
  • notes : 동작에 대한 설명을 정의한다.
  • authorizations : 뒤에서 언급할 인증 관련 값을 정의한다. (키 관련)

  • ApiController.java
package com.sample.swagger.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.sample.swagger.data.data;
import com.sample.swagger.data.dataParam;
import com.sample.swagger.data.dataRes;
import com.sample.swagger.response.ApiErr;
import com.sample.swagger.response.ApiNoAuth;
import com.sample.swagger.response.ApiRes;

import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Authorization;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "user", description = "get user list")
@RestController
@RequestMapping("/api")
public class ApiController {

    @Tag(name = "user")
    @ApiOperation(value = "유저 리스트", notes = "파라미터로 넘어온 수 만큼 유저를 리턴한다.", authorizations = {
            @Authorization(value = "apiKey") })
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = ApiRes.class))),
            @ApiResponse(responseCode = "201", description = "500과 동일"),
            @ApiResponse(responseCode = "401", description = "권한 없음(키누락)", content = @Content(schema = @Schema(implementation = ApiNoAuth.class))),
            @ApiResponse(responseCode = "403", description = "500과 동일"),
            @ApiResponse(responseCode = "404", description = "500과 동일"),
            @ApiResponse(responseCode = "500", description = "요청 실패", content = @Content(schema = @Schema(implementation = ApiErr.class)))
    })
    @GetMapping(value = "/getUser", produces = "application/json")
    public ResponseEntity<ApiRes<dataRes>> getUser(@RequestParam int num) {
        dataRes res = new dataRes();
        List<data> result = new ArrayList<data>();
        for (int i = 0; i < num; i++) {
            data d = new data();
            d.setUserNo(i);
            d.setUsername("user" + i);
            result.add(d);
        }
        res.setDataList(result);
        return new ResponseEntity<ApiRes<dataRes>>(new ApiRes<dataRes>(res), HttpStatus.OK);
    }

    @Tag(name = "user")
    @ApiOperation(value = "유저 리스트", notes = "파라미터로 넘어온 수 만큼 유저를 리턴한다.", authorizations = {
            @Authorization(value = "apiKey") })
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "요청 성공", content = @Content(schema = @Schema(implementation = ApiRes.class))),
            @ApiResponse(responseCode = "201", description = "500과 동일"),
            @ApiResponse(responseCode = "401", description = "권한 없음(키누락)", content = @Content(schema = @Schema(implementation = ApiNoAuth.class))),
            @ApiResponse(responseCode = "403", description = "500과 동일"),
            @ApiResponse(responseCode = "404", description = "500과 동일"),
            @ApiResponse(responseCode = "500", description = "요청 실패", content = @Content(schema = @Schema(implementation = ApiErr.class)))
    })
    @PostMapping(value = "/getUser", produces = "application/json")
    public ResponseEntity<ApiRes<dataRes>> postUser(@RequestBody dataParam param) {
        dataRes res = new dataRes();
        List<data> result = new ArrayList<data>();
        for (int i = 0; i < param.getNum(); i++) {
            data d = new data();
            d.setUserNo(i);
            d.setUsername("user" + i);
            result.add(d);
        }
        res.setDataList(result);
        return new ResponseEntity<ApiRes<dataRes>>(new ApiRes<dataRes>(res), HttpStatus.OK);
    }
}

응답에 작성된 클래스를 지정하고 돌려보면 가끔 오류로 찾지 못한다는 메세지가 뜰 수 있는데 이럴때는 설정 쪽을 건드려서 해당 클래스를 추가해주면 된다.

추측으로는 실제 컨트롤러에서 리턴하는 클래스가 아니기 때문에 잡지 못하는게 아닌가 한다.

  • SwaggerConfig.java
@Bean
public Docket api(TypeResolver typeResolver) {
	return new Docket(DocumentationType.OAS_30)
	// * 실제 에러 처리로 리턴하는 클래스를 명시하고자 할 때 해당 모델을 추가해준다.
	.additionalModels(
	typeResolver.resolve(ApiRes.class),
	typeResolver.resolve(ApiErr.class),
	typeResolver.resolve(ApiNoAuth.class))
	// * 스키마 멤버 변수 중, Date 관련 변수 문제로 인해 설정 추가
	.directModelSubstitute(LocalDate.class, java.sql.Date.class)
	.directModelSubstitute(LocalDateTime.class, java.util.Date.class)
	.useDefaultResponseMessages(true)
	.apiInfo(apiInfo())
	.select()
	.apis(RequestHandlerSelectors.basePackage("com.sample.swagger.controller"))
	.paths(PathSelectors.any())
	.build();
}

이렇게 까지만 작성하고 로컬 구동한 뒤, http://localhost:8080/swagger-ui/index.html 을 접속하면 다음과 같은 화면을 만나볼 수 있다.

swagger1

swagger2

4. 키 관련 설정

Swagger에서 가장 큰 특징 중 하나는 키 관련 설정을 제공한다는 것이다. 찾아보면 실제 토큰 값 또는 처리되는 방식에 따라 다르게 적용하는 방법이 있다.

우선 현재 샘플로 작성한 프로젝트에서는 단순히 헤더에 특정 값이 있는지를 체크할 것이기 때문에 단순하게 진행했다. 설정은 남겨놓았으니 참고 하면 된다.

  • SwaggerConfig.java
package com.sample.swagger.config;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import com.fasterxml.classmate.TypeResolver;
import com.sample.swagger.response.ApiErr;
import com.sample.swagger.response.ApiNoAuth;
import com.sample.swagger.response.ApiRes;

import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@EnableWebMvc
@Configuration
public class SwaggerConfig extends WebMvcConfigurationSupport {

    @Bean
    public Docket api(TypeResolver typeResolver) {
        return new Docket(DocumentationType.OAS_30)
                // * 실제 에러 처리로 리턴하는 클래스를 명시하고자 할 때 해당 모델을 추가해준다.
                .additionalModels(
                        typeResolver.resolve(ApiRes.class),
                        typeResolver.resolve(ApiErr.class),
                        typeResolver.resolve(ApiNoAuth.class))
                // * 스키마 멤버 변수 중, Date 관련 변수 문제로 인해 설정 추가
                .directModelSubstitute(LocalDate.class, java.sql.Date.class)
                .directModelSubstitute(LocalDateTime.class, java.util.Date.class)
                .useDefaultResponseMessages(true)
                .apiInfo(apiInfo())
                // 인증 토큰 방식이 있을때만 사용.
                // .securityContexts(Arrays.asList(securityContext()))
                .securitySchemes(Arrays.asList(apiKey()))
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sample.swagger.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger 3.0 Api Sample")
                .description("This is Sample")
                .version("1.0")
                .build();
    }

    // 인증 토큰 방식이 있을때만 사용.
    /*
     * private SecurityContext securityContext() {
     * return SecurityContext.builder()
     * .securityReferences(defaultAuth())
     * .build();
     * }
     */

    // 인증 토큰 방식이 있을때만 사용.
    /*
     * private List<SecurityReference> defaultAuth() {
     * AuthorizationScope authorizationScope = new AuthorizationScope("global",
     * "accessEverything");
     * AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
     * authorizationScopes[0] = authorizationScope;
     * return Arrays.asList(new SecurityReference("Authorization",
     * authorizationScopes));
     * }
     */

    private ApiKey apiKey() {
        return new ApiKey("apiKey", "apiKey", "header");
    }
}

위에서 중요한 부분은 apiKey() 부분인데, 파라미터 순서대로 name, keyname, passAs 이다. 이를 참고하여 작성하고 아래와 같이 컨트롤러의 @ApiOperation 부분에 내부 옵션으로 지정하면 된다

  • ApiController.java
@ApiOperation(value = "유저 리스트", notes = "파라미터로 넘어온 수 만큼 유저를 리턴한다.", authorizations = {@Authorization(value = "apiKey") })

해당 작업을 완료하고 다시 프로젝트를 돌려 확인해보면 아래와 같이 버튼이 추가된 것을 볼 수 있다.

swagger3

해당 버튼을 클릭하면 아래와 같이 팝업이 뜬다.

swagger4

여기에 값을 입력 후, Authorize를 클릭하면, 해당 값이 저장되어 테스트 시 같이 전달 된다.

swagger5

되게 간단하게 한다고 시작했는데 정리하는데 생각보다 시간이 많이 걸렸다. 무엇보다 스프링 부트에서 어노테이션으로 작업하는 예제가 많이 없어서 그렇기도 하고, 버전에 따라 옵션 값이나 어노테이션이 달라지는 부분도 있는데 찾기가 매우 힘들었다.

Swagger는 이렇게 한 번 정리 해놓으면 편하게 사용할 수 있으니 한 번 쯤은 시도해볼 만 한 가치가 있다고 본다.

해당 포스트의 예제 소스는 아래에서 보실 수 있습니다.

https://github.com/Chiptune93/spring-example/tree/main/Swagger/3.0/swagger


© 2024. Chiptune93 All rights reserved.