Spring Boot + Swagger 3.0 설정 + JWT 인증 설정 + @Profile로 환경별 설정

u-nij·2022년 12월 14일
2
post-thumbnail
post-custom-banner

개발 환경

Spring Boot 2.7.3
Gradle

Swagger란?

Open Api Specification(OAS), 이는 RESTful API spec을 정의된 규칙에 맞게 json이나 yaml로 표현하는 방식을 의미한다. Swagger는 OAS를 위한 프레임워크이며, API들이 갖고 있는 specification을 정의할 수 있는 툴들 중 하나이다. API의 문서를 자동화뿐만 아니라, 파라미터를 넣어보고 테스트를 진행할 수 있다. API 문서를 작성하는 시간을 절약할 수 있고, API 정보를 실시간으로 유지할 수 있다는 장점이 있다.

Springfox와 Springdoc

Spring에서 Swagger를 쉽게 사용할 수 있도록 도와주는 라이브러리로 Springdoc과 Springfox가 존재한다. 현재 2022년을 기준으로 사람들은 Springfox를 더 많이 사용하고 있기 때문에 Springfox를 적용해보겠다.

적용

build.gradle에 의존성 추가

	implementation 'io.springfox:springfox-boot-starter:3.0.0'

SwaggerConfig

@Configuration
public class SwaggerConfig {

    private Docket testDocket(String groupName, Predicate<String> selector) {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(this.apiInfo(groupName)) // ApiInfo 설정
                .useDefaultResponseMessages(false)
                .groupName("testApi")
                .select()
                .apis(RequestHandlerSelectors.
                        basePackage("패키지명"))
                .paths(PathSelectors.ant("/api/**")).build();
    }
    
    private ApiInfo apiInfo() {
      	return new ApiInfoBuilder()
                .title("제목")
                .description("설명")
                .version(version)
                .contact(new Contact("이름", "홈페이지 URL", "e-mail"))
                .build();
    }
}
  • 3.0으로 넘어오면서 @EnableSwagger2 는 사용하지 않아도 된다.
  • Docket: Swagger 설정의 핵심이 되는 Bean이다.
  • groupName(): 만약 Docket이 하나라면 생략이 가능하지만, 여러 개라면 groupName이 충돌해 오류가 발생하기 때문에 groupName을 명시해주어야 한다.
  • select(): ApiSelectorBuild 클래스의 인스턴스를 반환한다.
  • useDefaultResponseMessages(): true로 설정하면 Swagger에서 제공해주는 기본 응답 코드를 보여준다.
  • apis(): API가 작성되어 있는 Controller 패키지를 지정한다.(예: com.example.demo.XXXApiController) 만약, RequestHandlerSelectors.any()로 설정한다면 전체 API에 대한 문서를 Swagger를 통해 나타낼 수 있다.
  • paths(): 나타내고자 하는 API path를 작성한다. PathSelectors.any()로 설정한다면 패키지 안의 모든 API에 대한 문서를 나타낼 수 있다.

Controller 작성

이제 Swagger에 나타낼 Controller를 작성해보겠다.

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.Data;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

//Controller
@Api(tags = "Controller 이름")
@RestController
public class TestApiController {

    @Operation(summary = "요약", description = "설명")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "OK"),
            @ApiResponse(responseCode = "500", description = "Server Error")
    })
    @PostMapping("/api/test")
    public ResponseDto exampleMethod(
            @Parameter(description = "파라미터 설명", example = "1") @Valid RequestDto requestDto
    ) {
        return new ResponseDto();
    }
}

// RequestDto
@Data
class RequestDto {
    @ApiModelProperty(value="값1 설명", example="0")
    @NotNull
    private Long reqVar1;

    @ApiModelProperty(value="값2 설명", example="example string")
    private String reqVar2;
}

// ResponseDto
@Data
class ResponseDto {
    @ApiModelProperty(value="값1 설명", example="0")
    private Long resVar1;

    @ApiModelProperty(value="값2 설명", example="example string", hidden = true)
    private String resVar2;
}

각각의 어노테이션에 다양한 값이 있어 자세히 나타낼 수 있다. 일부만 간단하게 작성해보았다.

  • @Api: tags 값으로 Swagger에 나타낼 Controller의 이름을 지정했다.
  • @Operation: API에 대한 요약 정보(summary)와 설명(decription)을 작성했다.
  • @ApiResponse: API의 HTTP Status Code 반환 값에 대한 설명을 작성했다.
    • @ApiResponses 어노테이션을 이용해 여러 개의 반환 값을 작성할 수 있다.
  • @Parameter: 파라미터에 대한 설명(decription)와 예시(example)을 작성했다.
  • @ApiModelProperty: DTO 필드에 대한 설명(decription)와 예시(example)을 작성했다.

애플리케이션을 실행시키고 http://localhost:8080/swagger-ui/index.html로 접속해 Swagger를 실행시키면 작성한 API에 대한 문서가 생성된 것을 볼 수 있다.

Controller 부분과 Schemas 부분을 간단히 들여다보겠다. 직관적이라 쉽게 이해할 수 있다.

Controller

@Valid 어노테이션에 의해 RequestDto의 @NotNull 어노테이션이 적용되어 resVar1 값 옆에 *required라고 명시된 것을 확인할 수 있다.
@ApiResponse로 작성한 반환 값들에 대한 설명을 확인할 수 있다. Docket 객체의 useDefaultResponseMessages() 값을 true로 설정해두었다면 200, 401, 403, 404에 대한 기본 응답 메세지 또한 확인할 수 있다.

Schemas

응답 데이터에 대한 정보를 확인할 수 있다. reqVar2 값을 hidden=true로 설정해두었기 때문에 화면에 보이지 않는다. 요청DTO에도 설정해둘 수 있다.

실행

왼쪽 상단의 "Try it out"을 눌러 API를 실행해보겠다.

ApiInfo

ApiInfo 객체를 이용해 Swagger 위에 나타나는 부분을 커스터마이징 할 수 있다.

@Configuration
public class SwaggerConfig {

    private Docket testDocket(String groupName, Predicate<String> selector) {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(this.apiInfo(groupName)) // ApiInfo 설정
                .useDefaultResponseMessages(false)
                .groupName("testApi")
                .select()
                .apis(RequestHandlerSelectors.
                        basePackage("패키지명"))
                .paths(PathSelectors.ant("/api/**")).build();
    }
    
    private ApiInfo apiInfo() {
      	return new ApiInfoBuilder()
                .title("제목")
                .description("설명")
                .version(version)
                .contact(new Contact("이름", "홈페이지 URL", "e-mail"))
                .build();
    }
}

생성자를 이용해 모든 정보를 넣거나, 혹은 build() 메소드를 통해 원하는 정보를 넣어 객체를 생성할 수 있다.

JWT를 사용하기 위한 설정

SwaggerConfig

@Configuration
public class SwaggerConfig {

    private Docket testDocket(String groupName, Predicate<String> selector) {
        return new Docket(DocumentationType.OAS_30)
                .useDefaultResponseMessages(false)
                .securityContexts(List.of(this.securityContext())) // SecurityContext 설정
                .securitySchemes(List.of(this.apiKey())) // ApiKey 설정
                .groupName("testApi")
                .select()
                .apis(RequestHandlerSelectors.
                        basePackage("패키지명"))
                .paths(PathSelectors.ant("/api/**")).build();
    }
    
    // JWT SecurityContext 구성
    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 List.of(new SecurityReference("Authorization", authorizationScopes));
    }
    
    // ApiKey 정의
    private ApiKey apiKey() {
        return new ApiKey("Authorization", "Authorization", "header");
    }
}

저는 JWT 토큰 방식을 이용해 Authorization Header를 "Bearer {Access Token}" 형식으로 받아와 인증을 처리했기 때문에, 이 글을 보시는 분들과 ApiKey를 정의하는 방식이 다를 수 있습니다.

  • SecurityContext: 인증하는 방식을 설정한다. 전역 AuthorizationScope를 사용해 SecurityContext를 구성했다.
  • ApiKey: Swagger 내에서 인증하는 방식으로, ApiKey는 JWT, Bearer, Authorization이 있다. Authorization을 인증 헤더로 포함하도록 ApiKey를 정의했다.(2번째 인자의 Authorization이 중요함!)

실행

Swagger를 실행해보면 오른쪽에 자물쇠 모양의 버튼이 생성된 것을 확인할 수 있다.
토큰을 입력하고 Authorize 버튼을 누르면 API에 대한 모든 요청이 HTTP 헤더에 토큰이 자동으로 포함된다.
인증 후, 자물쇠가 잠겨있는 것을 확인할 수 있다.

환경별 Swagger 작동 처리

시행착오

운영 환경에서는 Swagger가 작동하지 않도록 처리하는 것이 필요하다. 이 문제 때문에 며칠동안 고민이 많았는데, SwaggerConfig 클래스에 @Profile 어노테이션을 달았는데도 우선 "작동"이 된다는 점이었다.

SwaggerConfig

@Profile({"!prod"})
@Configuration
public class SwaggerConfig {
	// ...
}

예상대로라면 prod 값일 때는 Swagger가 작동되지 않을 줄 알았다.

운영 환경

spring.profiles.active: prod

개발 환경

spring.profiles.active: dev

??????
.. 그리고 모든 Controller가 똑같이 보이고 똑같이 실행됐다. SecurityConfig에서 IP 등을 사용해 접근을 직접 제어하는 방법도 사용해보았지만, IP가 바뀔 때도 있을 것이고, 손이 많이 가는 방법이라 사용하고 싶지 않아서 더 고민을 하게 됐다.(API 문서 개발이 급해 그냥 커밋해버리고 싶다는 생각을 12129038109번정도 했다🤤...)

찾아낸 방법👍

사실 이거 기록해두려고 글을 작성한 것 같다.. 구글링을 열심히 하다가!! 이 글을 보고 방법을 참고해 해결했다!! Docket 객체의 enabled() 메소드를 활용하는 방법이었다.

@Configuration
public class SwaggerConfig {

    @Profile({"test || dev"})
    @Bean
    public Docket indexApi() {
        return docket("default", PathSelectors.ant("/api/**"));
    }

    @Bean
    @Profile({"!test && !dev"})
    public Docket disable() {
        return new Docket(DocumentationType.OAS_30).enable(false);
    }

운영 환경에서 아예 Swagger 페이지에 접근이 불가능해진다!!!

이상 Swagger 적용기였다.. 끝까지 글을 봐주신 분들 감사합니다!

profile
삶은 달걀이다
post-custom-banner

0개의 댓글