구현하기

구현 순서는 아래와 같다.

  1. restdocs-api-spec을 이용한 OAS 파일을 생성하는 빌드 환경 구축
  2. Swagger-UI standalone, Static Routing 세팅
  3. 생성된 OAS 파일을 Swagger 디렉터리로 복사(copyOasToSwagger)
  4. SampleController 및 MockMvc REST Docs Test 코드 작성
  5. 결과 확인

사용 기술

  • Spring Boot 3.3.0 / gradle-kotlin
  • Java 17
  • restdocs-api-spec

1. restdocs-api-spec을 이용한 OAS 파일을 생성하는 빌드 환경 구축

1-1. 의존성 추가

아래처럼 의존성을 추가해준다.

// build.gradle.kts
plugins {
    ... 생략
    id("com.epages.restdocs-api-spec") version "0.15.3"
}

repositories {
    mavenCentral()
}

dependencies {
    ... 생략
    // Rest Docs
    testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
    testImplementation("org.springframework.restdocs:spring-restdocs-asciidoctor")
    testImplementation("com.epages:restdocs-api-spec-mockmvc:0.17.1")
    ... 생략
}

openapi3 {
    this.setServer("https://localhost:8080") // list로 넣을 수 있어 각종 환경의 URL을 넣을 수 있음!
    title = "My API"
    description = "My API description"
    version = "0.1.0"
    format = "yaml" // or json
}

1-2. task 확인

의존성을 추가하고 gradle의 tasks를 보면 아래 사진처럼 openapi3 과 같은 task가 추가돼있다.

 

여기서 openapi3 task를 실행해보면 아래 사진처럼 build/api-spec 경로에 openapi3.yaml 파일이 생성된다.

 

openapi3.yaml 파일을 보면 아래와 같다.


2. Swagger-UI standalone, 리소스 핸들러 설정

2-1. Swagger-ui 정적 파일 설치

위 사진은 Swagger-ui 정적 파일 설치 사이트 하단의 설명이다.

위 페이지에서 latest release 를 다운 받는 링크로 이동하여 압축 파일을 받아준다.

2-2. 스프링부트의 정적 파일 경로에 추가

압축을 해제하면 아래 사진처럼 나오는데, 여기서 /dist 의 내용물만 필요하다.

 

/dist 내용물을 아래 사진처럼 스프링부트의 /resources/static/swagger-ui 로 옮겨준다.

2-3. 파일명 변경 및 불필요한 파일 제거

index.html 은 swagger-ui.html 로 파일명을 변경하였다.

또한 불필요한 파일들은 삭제해주었다.

  • oauth2-redirect.html
  • swagger-ui.js
  • swagger-ui-es-bundle-core.js
  • swagger-ui-es-bundle.js

2-4. swagger-initializer.js 파일의 url 수정

swagger-initializer.js의 코드를 보면 아래 사진과 같다.

1-2 에서는 openapi3 task 를 돌리면 OAS 파일이 생성됐지만, 최종적으로는

Spring Rest Docs와 연동하여 빌드 시 테스트 코드를 돌릴 때 OAS 파일을 만들게 될것이다.

swagger-initializer.js에서 OAS 파일을 읽어 API 시각화를 하기 때문에 이를 우리가 만들 OAS 파일을 참조하도록 수정한다.

여기서 url 부분을 수정하여 우리가 만들 OAS 파일을 참조하도록 한다.

Swagger-initializer.js

2-5. 리소스 핸들러 설정

브라우저에서 스프링부트 애플리케이션 서버의 /static/swagger-ui 경로에 있는 정적 파일에 접근하기 위해서는

리소스 핸들러 설정을 바꿔줘야 한다.

(사실 이 부분은 2-4 에서 url 을 /static/swagger-ui/openapi3.yaml 으로 했을때는 필요했는데

본인은 url을 /swagger-ui/openapi3.yaml로 설정해서 굳이 필요 없을것 같기는 하다.)

@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
    }
}

3. 생성된 OAS 파일을 Swagger 디렉터리로 복사(copyOasToSwagger)

1-2에서 openapi3 task를 실행하면 build/api-spec 경로에 openapi3.yaml 파일이 생성되는걸 확인했다.

이걸 2에서 생성한 /static/swagger-ui 경로에 넣고 swagger-ui.html로 읽을 것이다.

build.gradle.kts에 아래 코드를 추가하여 이를 위한 task를 추가한다.

// build.gradle.kts

tasks.withType<Test> {
    useJUnitPlatform()
    // 기존에 있는 test 쪽에 이 코드를 추가
    finalizedBy("copyOasToSwagger")  // test 후 copyOasToSwagger task 진행
}

val buildDir = layout.buildDirectory.get().asFile
tasks.register<Copy>("copyOasToSwagger") {

    dependsOn("openapi3") // openapi3 Task가 먼저 실행되도록 설정
    delete("src/main/resources/static/swagger-ui/openapi3.yaml") // 기존 OAS 파일 삭제
    from("$buildDir/api-spec/openapi3.yaml") // 복제할 OAS 파일 지정
    into("src/main/resources/static/swagger-ui/") // 타겟 디렉터리로 파일 복제
}

tasks {
    bootJar {
        // openapi3.yaml 파일을 /static/swagger-ui 경로로 복사에 성공한 후 bootJar 진행
        dependsOn("copyOasToSwagger")
        from("build/api-spec") {
            into ("BOOT-INF/classes/static/swagger-ui")
        }
    }
}

 

위 코드를 추가하고 빌드나 테스트를 돌려서 로그를 살펴보면 아래처럼 task 를 진행함을 확인할 수 있다.

test -> openapi3 -> copyOasToSwagger -> bootJar -> build 의 순서로 task 를 진행한다.


4. Sample Controller 코드 및 Rest Docs 테스트 작성

간단하게 테스트용으로 컨트롤러를 작성하였다.

TestController

@RestController
@RequestMapping("/hi")
public class TestController {

    @GetMapping("/hi1")
    public String test() {
        return "hi1";
    }

    @GetMapping("/hi2")
    public String test2() {
        return "hi2";
    }
}

TestControllerTest

Spring Rest Docs는 MockMvc 와 결합해서 동작한다.

컨트롤러 테스트 코드를 작성할 때 아래 두 가지를 목적에 맞게 사용해야 한다.

  • Rest Docs 사용- MockMvcRestDocumentation.document 
  •   Swagger 사용 - MockMvcRestDocumentationWrapper.document

본인은 Rest Docs는 사용하지 않고 Swagger 용도로만 사용하였다.

어떤 메서드를 사용하는지 참고하기 쉽도록 패키지까지 올려두었다.

package com.example.dream;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith(RestDocumentationExtension.class)
class TestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @DisplayName("")
    @Test
    void test() throws Exception {
        mockMvc.perform(get("/hi/hi1")
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("나는짱1"));
    }

    @DisplayName("")
    @Test
    void test2() throws Exception {
        mockMvc.perform(get("/hi/hi2")
                .accept(MediaType.APPLICATION_JSON)
            )
            .andDo(print())
            .andExpect(status().isOk())
            .andDo(document("나는짱2"));
    }
}

 


5. 결과 확인

아래 사진처럼 Production 코드를 침투하지 않고도 Swagger 의 API Test가 가능하다.

또한 Spring Rest Docs 의 테스트 코드를 통한 유지 보수성 또한 확보 되었다.

 

위 결과는 빌드하고 java -jar 명령어로 실행해본 결과이다.

배포 환경에서는 서버 URL 설정을 변경해줘야할 수 있다.

그때는 build.gradle.kts 의 openapi3 쪽의 서버 URL을 변경해주자.

혹은 주석 달려있는것 처럼 List 형태로 넣어서 여러개의 URL을 사용해도 될듯 하다.

yml 파일로 동적으로 이용할 수 있는 방법도 있을듯 하니 추후 사용할 때 가볍게 정리할것 같다.

 

참고자료

 

OpenAPI Specification을 이용한 더욱 효과적인 API 문서화 | 카카오페이 기술 블로그

사실상의 표준으로 발돋움 중인 OpenAPI Specification을 이용한 API 문서화 방법(Swagger와 Spring REST Docs의 장점을 합치는 방법)을 공유드립니다.

tech.kakaopay.com

 

 

내가 만든 API를 널리 알리기 - Spring REST Docs 가이드편

'추석맞이 선물하기 재개발'에 차출되어 API 문서화를 위해 도입한 Spring REST Docs 를 소개합니다.

helloworld.kurly.com

 

 

[리팩토링] Swagger UI + Spring RestDocs 적용기

Swagger UI 와 Spring RestDocs 의 장 단점의 비교Swagger UI 의 장점직관적인 UI스웨거는 API 에 대한 요청과 응답 등을 시각적으로 표현하여 사용자가 쉽게 이해할 수 있습니다실시간 테스트API 엔드포인트

oth3410.tistory.com

 

API 문서화

개발자에게는 API를 만드는것 뿐 아니라 이에 대한 문서화 또한 필요하다.

수동으로 Notion이나 스프레드 시트에 정리할 수도 있겠지만 이는 일일이 최신화를 해줘야해서 너무 불편하다.

이런 불편함을 해소하기 위해 API 문서화를 자동으로 해주는 툴이 나왔고, 유명한건 아래 두가지 정도가 있다.

  • Spring Rest Docs
  • Swagger

한번 이 두가지를 비교해보자.


Spring Rest Docs  vs  Swagger

Spring Rest Docs

Spring Rest Docs는 컨트롤러 테스트를 통해 Integration 테스트를 진행하면서 최신화하는 형식이다.

테스트 코드를 통한 방식이기 때문에 Production 코드에 영향을 미치지 않아서 깔끔하게 분리가 가능하다.

또한 테스트를 강제하기 때문에 유지보수에 도움을 준다.

하지만 Swagger처럼 API Test 기능을 지원하지 않고 가시성이 조금 부족하다는 단점이 있다.

아래는 Rest Docs의 예시 이미지이다.

Swagger

Swagger는 컨트롤러나 DTO 등에 애노테이션을 다는걸로 API 문서를 만들어준다.

이전 글에서 Swagger를 써서 API 문서화를 해보았다.

Swagger는 API Test 기능을 지원하고 문서의 가시성이 좋다.

아래는 Swagger의 예시 이미지이다.

 

하지만 Swagger 문서화를 위한 코드가 Production 코드에 너무 깊게 침투한다.

그래서 아래처럼 컨트롤러 코드가 너무 지저분해진다.


Spring Rest Docs + Swagger 병용

Swagger를 쓰면서 늘 불편했던 컨트롤러 코드가 지저분해지는 단점을 커버할 수 있단 말에 굉장히 설렜다.

이 방법에 대한 간단한 설명은 아래와 같다.

OpenApi Specification(OAS) 기반 API 문서화

Swagger 팀이 SmartBear Software에 합류하면서 Swagger Spec.이 OpenApi Spec.으로 명칭이 바뀌었고,

오늘날에는 RESTful API 스펙에 대한 사실상의 표준으로서 활용되고 있다고 한다.

Swagger-ui는 이 OAS를 해석하여 API 스펙을 시각화해준다.

또한 Postman, Paw 같은 API Client들도 OAS를 지원하고 있어 OAS를 사용하면 활용도가 높을것으로 예상된다.

OAS를 생성하는 restdocs-api-spec 오픈소스 

그렇다면 Spring REST Docs와 Swagger의 장점을 어떻게 합칠 수 있을까? 

독일 기업 epages에서 Spring REST Docs를 연동하여 OAS 파일을 만들어주는 오픈소스(restdocs-api-spec)를 제공하고 있다.

위에서 Swagger-ui는 OAS를 해석해서 API 스펙을 시각화 해준다고 했다.

따라서 이 오픈소스를 이용해서 OAS 파일을 생성하고 Swagger-ui로 띄우면 된다!

 

 

최근에 한 팀 프로젝트에서는 Swagger를 사용하였는데, Production 코드에 침투하는게 마음에 들지는 않았지만 Swagger의 장점 때문에 유지했었다. (사실 팀원이 테스트 코드를 잘 작성하지 않아서 RestDocs 를 쓰기는 힘들기도 했다.)

하지만 이처럼 Spring Rest Docs와 Swagger를 혼용하여 장점만 취하는 방법을 접하였고 다음 글에서 시도해보려 한다.

API 인증 필요 여부 개별 관리

이전 글에서 API 인증 필요 여부를 표기하려면 @SecurityRequirement 를 이용했었다.

하지만 이 애노테이션을 클래스에 붙이면 해당  클래스 아래의 모든 엔드포인트에 자물쇠가 표시된다.

만약 그 중 1~2개 정도는 인증이 필요 없어서 자물쇠 표시를 하지 않으려면 어떡 해야할까?

애노테이션을 메서드 단위로 붙여서 인증이 필요 없는 메서드만 안붙이기에는 너무 번거롭다.

이런 상황을 위해 API 인증이 불필요한 메서드에만 애노테이션을 붙여서 자물쇠를 제거하는 방법을 정리해두려 한다.


구현하기

구현 순서는 아래와 같다.

  1. 커스텀 애노테이션 추가
  2. SwaggerConfig 추가 설정
  3. 컨트롤러에 커스텀 애노테이션 추가
  4. 결과 확인

1. 커스텀 애노테이션 추가

SecurityNotRequired 라는 이름의 애노테이션을 추가해준다.

이름은 원하는대로 설정해도 상관 없다.

SecurityNotRequired

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecurityNotRequired {
}

2. SwaggerConfig 추가 설정 

기존 설정에서 아래처럼 OperationCustomizer 를 빈등록해준다.

여기서 애노테이션을 등록해줄 때는 본인이 만든 커스텀 애노테이션으로 등록해주면 된다.

SwaggerConfig

... 기존 설정

@Bean
public OperationCustomizer customize() {
    return (Operation operation, HandlerMethod handlerMethod) -> {
        SecurityNotRequired annotation = handlerMethod.getMethodAnnotation(SecurityNotRequired.class);
        // SecurityNotRequire 어노테이션있을시 스웨거 시큐리티 설정 삭제
        if (annotation != null) {
            operation.security(Collections.emptyList());
        }
        return operation;
    };
}

3. 컨트롤러에 커스텀 애노테이션 추가

클래스에 @SecurityRequirement 애노테이션을 추가하고

인증이 필요 없는 엔드포인트에만 @SecurityNotRequired 애노테이션을 추가하였다.

4. 결과 확인

스웨거 UI를 보면 아래처럼 애노테이션을 추가해준 부분에는 자물쇠 표시가 사라져있음을 확인할 수 있다.

HTTPS에서 스웨거

보통 프로젝트를 진행하면서 서비스를 배포하고 도메인을 붙이고 HTTPS를 적용할 것이다.

하지만 스웨거의 기본 서버 URL은 http://localhost:8080 이므로

HTTPS를 적용할 때 스웨거에서 설정을 추가해주지 않으면 이 글처럼 CORS 에러를 만나는 수가 있다.

따라서 환경에 따라 스웨거 서버 설정을 변경해주어야 한다.


1.  SwaggerConfig 설정 추가

SwaggerConfig

아래처럼 서버 설정을 추가하고 환경변수로 분리하여 환경에 맞게 관리한다.

@Configuration
@RequiredArgsConstructor
@SecurityScheme(
    name = "jwt-cookie",
    type = SecuritySchemeType.APIKEY,
    in = SecuritySchemeIn.COOKIE
)
public class SwaggerConfig {

    private final AppConfig appConfig;

    @Bean
    public OpenAPI serverApiConfig() {
        Server server = new Server();
        server.setUrl(appConfig.getBackUrl()); // 여기서 서버 URL 설정 가능 -> 환경 변수로 관리
        server.description("백엔드 도메인");
        return new OpenAPI()
            .addServersItem(server)
            .info(new Info().title("PAWLAND API")
                .description("PAWLAND API SWAGGER UI입니다."));
    }
}

2. 결과 확인

아래처럼 스웨거 서버 URL을 yml으로 관리하여 프로필에 따라 환경 변수를 다르게 할 수 있다.

1. 로컬에서 테스트 시 서버 URL - application-local.yml

2. 배포 시 서버 URL - application-prod.yml

 

인증 필요 여부 표기

엔드 포인트 중에는 인증이 필요한 기능이 있고, 필요 없는 기능이 있다.

API 사용자 입장에서는 직접 사용해서 깨닫기 보다는 사용 전에 이 두가지를 구분해주면 사용하기 더 편리할 것이다.


구현하기

구현 순서는 아래와 같다.

  1. SwaggerConfig 추가 설정
  2. 컨트롤러에 스웨거 UI용 설정 추가
  3. 결과 확인

1.  SwaggerConfig 추가 설정 

아래처럼 SwaggerConfig에 @SecurityScheme 애노테이션을 이용하여 인증 수단을 등록할 수 있다.

본인은 JWT를 쿠키에 넣어서 인증하는 방식을 사용했다.

만약 Authorization 헤더를 사용한다면 알맞게 수정하면 된다.

SwaggerConfig

@Configuration
@RequiredArgsConstructor
@SecurityScheme(
    name = "jwt-cookie",
    type = SecuritySchemeType.APIKEY,
    in = SecuritySchemeIn.COOKIE
)
public class SwaggerConfig {

 


2.  컨트롤러에 스웨거 UI 표기용 설정 추가

UserController

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@SecurityRequirement(name = "jwt-cookie")
@Tag(name = "UserController", description = "유저 정보 관련 컨트롤러 입니다.")
public class UserController {

ㄴ@SecurityRequirement

해당 애노테이션 추가 시 인증이 필요하다는 자물쇠 모양이 생긴다.

name 값에는 SwaggerConfig 에서 @SecurityScheme 을 이용하여 등록한 인증 수단 이름을 넣어준다.

각각 메서드에도 붙일 수 있지만 클래스에 붙이면 모든 엔드포인트가 인증이 필요함을 표시할 수 있다.


3. 결과 확인

아래 사진처럼 인증이 필요한 컨트롤러에는 자물쇠 모양으로 구분할 수 있게 되었다.

스웨거

팀 프로젝트를 하면서 프론트엔드와 백엔드 간에 API 명세서가 필요해졌다.

전엔 스웨거를 안써서 노션에 수동으로 API 명세서를 작성하고 수정했었다.

그래서 최신화가 안되는것도 많았는데 이번엔 API 문서 자동화 툴인 스웨거를 이용해보기로 했다.


사용 기술

  • Spring Boot 3.2.4 / gradle-kotlin
  • Java 17
  • Swagger

1. 의존성 추가

스웨거를 사용하기 위해 의존성을 추가해준다.

스웨거도 버전이 여러가지인듯 하므로 적당히 취향껏 쓰면 될것 같다.

본인이 사용한 스웨거 라이브러리는 이쪽 링크에서 확인할 수 있다. 

// build.gradle.kts
// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4")

2.  SwaggerConfig 설정

스웨거를 사용하기 위해서는 Config 설정이 필요하다.

아래와 같은 기본 설정을 해주면 사용할 수 있다.

SwaggerConfig

@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {

    @Bean
    public OpenAPI serverApiConfig() {
        return new OpenAPI()
            .info(new Info().title("PAWLAND API")
                .description("PAWLAND API SWAGGER UI입니다."));
    }
}

3.  컨트롤러에 스웨거 UI 표기용 설정

UserController

스웨거는 기본적으로 아래처럼 애노테이션을 붙이는 식으로 UI에 표시한다.

참고로 스웨거를 사용하면 기본적인 요청, 응답 DTO는 알아서 맵핑해준다.

@ModelAttribute 로 쿼리스트링을 여러개 받는 경우는 따로 애노테이션을 붙여야하지만 다음에 다뤄보겠다.

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@Tag(name = "UserController", description = "유저 정보 관련 컨트롤러 입니다.")
public class UserController {

    private final UserService userService;

    @PreAuthorize("hasRole('ROLE_USER')")
    @Operation(summary = "내 정보 조회", description = "JWT로 내 정보를 조회합니다.")
    @ApiResponse(responseCode = "200", description = "내 정보 조회 성공")
    @ApiResponse(responseCode = "500", description = "삭제된 회원 또는 잘못된 JWT")
    @GetMapping(value = "/my-info", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<UserInfoResponse> getUserInfo(@AuthenticationPrincipal UserPrincipal userPrincipal) {
        UserInfoResponse userInfo = userService.getUserInfo(userPrincipal.getUsername());
        return ResponseEntity
                .status(OK)
                .body(userInfo);
    }
}

 

ㄴ@Tag

아래 사진에서 보듯 컨트롤러의 최상단의 그룹 이름과 설명을 표시할 수 있다.

ㄴ@Operation

각각의 컨트롤러에 대한 이름과 설명을 표시할 수 있다.

ㄴ@ApiResponse

각각의 응답 코드에 따른 설명을 표시할 수 있다.


4. 결과 확인

포트번호가 8080 기준으로 http://localhost:8080/swagger-ui/index.html 로 들어가면 스웨거를 확인할 수 있다.

+ Recent posts