본문 바로가기
프로젝트/기록

API 문서화 - RestDocs 와 Swagger 함께 사용하기

by 데브조이 2024. 8. 8.
반응형

개요

API 문서화 도구인 RestDocs와 Swagger 는 장단점이 매우 명확하다.
 
Swagger는 어노테이션을 추가해서 빠르게 API 명세를 작성할 수 있고, 웹 UI와 API 테스트를 제공하는 매우 큰 장점이 있다.
하지만 애플리케이션 코드에 API 명세 목적의 코드가 추가되기 때문에 다소 지저분해질 수 있다.
 
RestDocs는 테스트 코드 작성을 통해 API 명세를 작성할 수 있다.
테스트 코드를 강제하고, 애플리케이션 코드 (비지니스 로직) 과 명세 목적의 코드를 분리할 수 있다는 장점이 있다.
하지만 일부 문서를 수동으로 작성해야 한다.
 
나는 RestDocs가 테스트 기반으로 문서를 생성하기 때문에 신뢰성을 높일 수 있어서 좋았다.
그리고 애플리케이션 코드와 명세 목적 코드가 섞이는 것이 싫어서 Swagger는 공부할 때만 사용했었다.
 
그런데 보리 프로젝트를 시작하면서, API 문서화 도구에 대한 고민에 빠졌다.
클라이언트 개발자는 Swagger를 원했고, 나는 RestDocs를 선호한다.
 
API 명세서의 목적은 원활한 협업이기에 클라이언트 개발자의 의견을 적극 수용하고자 했다.
그런데 고맙게도 클라이언트 개발자가 전 직장에서 Swagger + RestDocs를 썼었다고 팁을 주었다.
 
그렇게 해서 Swagger + RestDocs 를 함께 사용하게 되었다.
이제 막강 조합 Swagger + RestDocs 로 API 문서화 하는 법을 알아보자.
 


개발 환경

  • Java 17
  • Spring Boot 3.3.2

 

사전 지식

  • Swagger Spec.이 OpenApi Spec. 으로 명칭이 변경되었다.
  • Swagger-UI 는 OpenApi Specification(OAS) 를 해석해서 API 스펙을 시각화 한다.
  • 독일 기업 epages 에서 Spring Rest Docs 연동을 통해 OAS 파일을 만들어주는 오픈 소스 제공한다 (restdocs-api-spec).

 

흐름

  • 컨트롤러 테스트 코드를 작성한다.
  • restdocs-api-spec 를 통해 OAS 파일을 생성한다.
  • swagger generator를 통해 OAS 파일로 Swagger UI를 생성한다.
  • Swagger UI를 build 디렉토리로 복사한다.
  • Swagger UI를 통해 API 명세서를 확인한다.

의존성 추가

가장 먼저 사용할 라이브러리들에 대한 의존성 설정이 필요하다.
아래는 설정파일 전체 코드이고, 순차적으로 하나씩 확인해보자.
 
build.gradle

import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
import org.springframework.boot.gradle.tasks.bundling.BootJar

buildscript {
    ext {
        restdocsApiSpecVersion = '0.17.1'
    }
}

plugins {
    // ...

    id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
    id 'org.hidetake.swagger.generator' version '2.18.2'
}

// ...


swaggerSources {
    sample {
        setInputFile(file("${project.layout.buildDirectory.get().asFile}/api-spec/openapi3.yaml"))
    }
}

openapi3 {
    servers = [
            { url = "http://localhost:8080" }
    ]
    title = "API 문서"
    description = "RestDocsWithSwagger Docs"
    version = "0.0.1"
    format = "yaml"
}

dependencies {
    // ...

    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
    swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

// ...

tasks.withType(GenerateSwaggerUI).configureEach {
    dependsOn 'openapi3'
}

tasks.register('copySwaggerUI', Copy) {
    dependsOn generateSwaggerUISample

    def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()

    from("${generateSwaggerUISampleTask.outputDir}")
    into("${project.layout.buildDirectory.get().asFile}/resources/main/static/docs")
}

tasks.named('resolveMainClassName') {
    dependsOn 'copySwaggerUI'
}

tasks.withType(BootJar) {
    dependsOn 'copySwaggerUI'
}

tasks.withType(Jar) {
    dependsOn 'copySwaggerUI'
}

 
 

buildscript {
    ext {
        restdocsApiSpecVersion = '0.17.1'
    }
}

restdocs-api-spec 라이브러리 버전을 지정해준다.
버전을 직접 적어줘도 무방하나, 여러 곳에서 사용하고 있어서 버전 변경이 용이하도록 속성으로 정의하였다.
 

buildscript{} : 빌드 스크립트가 실행될 때 사용되는 의존성이나 설정을 정의할 수 있다.
ext{} : 추가적인 속성을 정의하기 위해 사용하는 블록이다.

 

plugins {
    // ...
    id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
    id 'org.hidetake.swagger.generator' version '2.18.2'
}

restdocs-api-spec와 swagger generator 플러그인을 추가해준다.
 swagger generator 가 OpenAPI3 스펙을 기반으로 Swagger UI 를 생성해주는 역할을 한다.
 
 

swaggerSources {
    sample {
        setInputFile(file("${project.layout.buildDirectory.get().asFile}/api-spec/openapi3.yaml"))
    }
}

 
swaggerSources 설정을 추가해준다.
위 코드는 OpenApi 파일이 생성될 위치를 지정해주는 설정이다.
 

  • swaggerSources{} : Swagger 소스와 관련된 설정을 수행한다. 위 코드에서는 sample 소스에 대한 설정을 하고 있다.
  • setInputFile() : Swagger 소스 파일의 위치를 지정하는 메서드이다.
  • project.layout.buildDirectory.get().asFile : Gradle에서 프로젝트의 빌드 디렉토리 경로를 동적으로 가져오며, 빌드 디렉토리를 파일 객체로 반환한다.

참고 자료들은 빌드 디렉토리를 가져올 때 project.buildDir을 사용해서 찾아봤더니 현재는 deprecated 되었고, Project.layout.buildDirectory을 대신 사용하면 된다.

 
자세한 내용은 Upgrading your build from Gradle 8.x to the latest 을 참고하자.  
 

openapi3 {
    servers = [
            { url = "http://localhost:8080" }
    ]
    title = "API 문서"
    description = "RestDocsWithSwagger Docs"
    version = "0.0.1"
    format = "yaml"
}

 
OpenAPI 문서 설정을 정의하는 부분이다. 

  • openapi3 { }:  OpenAPI 3 API 문서 생성을 위한 설정을 정의한다. 서버 정보, 제목, 설명, 버전, 파일 형식 등을 설정할 수 있다.
  • servers = [{ url = "..." }]: API가 동작할 서버의 URL을 설정한다. 여러 URL 을 정의할 수 있다. 
  • title: OpenAPI 문서의 제목을 설정한다.
  • description: 문서에 대한 설명
  • version: API 버전 설정
  • format: OpenAPI 문서 형식 설정. 일반적으로 yaml, json 형식 사용.

 

dependencies {
    // ...

    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
    swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

 
spring-restdocs-mockmvc, restdocs-api-spec-mockmvc, swagger-ui 의존성을 추가해준다. 
만약 restassured를 사용한다면, restdocs-api-spec-restassured를 사용할 수 있다.  
 
 
 
 
 

tasks.withType(GenerateSwaggerUI).configureEach {
    dependsOn 'openapi3'
}

tasks.register('copySwaggerUI', Copy) {
    dependsOn generateSwaggerUISample

    def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()

    from("${generateSwaggerUISampleTask.outputDir}")
    into("${project.layout.buildDirectory.get().asFile}/resources/main/static/docs")
}

 
첫번째 tasks 블록은 GenerateSwaggerUI 작업이 실행되기 전에 openapi3 작업이 먼저 실행되도록 하는 코드이다.
OpenAPI3 문서가 먼저 생성되고 Swagger UI 를 생성해야 하기 때문에 넣어준 설정이다. 
 
두번째 tasks 블록은 Gradle에 copySwaggerUI 작업을 등록하는 코드이다. 
생성한 SwaggerUI를 build/resources/main/statifc/docs 경로로 복사하는 작업이다. 
 
이 작업을 통해 애플리케이션 실행 시, 서버URI/docs/index.html 에서 Swagger UI 를 확인할 수 있다.
 

tasks.named('resolveMainClassName') {
    dependsOn 'copySwaggerUI'
}

tasks.withType(BootJar) {
    dependsOn 'copySwaggerUI'
}

tasks.withType(Jar) {
    dependsOn 'copySwaggerUI'
}

 
첫번째 tasks 블록: resolvemainClassName 작업 전에 copySwaggerUI가 실행되도록 한다. Swagger UI  파일이 제대로 복사된 후 메인 클래스의 이름을 결정하도록 하는 것이다 .즉, SwaggerUI 파일이 준비된 상태에서 애플리케이션이 빌드되도록 설정한 것이다. 
두번째 tasks 블록: BootJar 작업 실행 전 Swagger UI 파일이 복사되도록 한다.
세번째 tasks 블록: Jar 작업 실행 전 Swagger UI 파일이 복사되도록 한다.
 
 


 

테스트 코드 작성

아래 코드는 테스트할 애플리케이션 코드이다.  
 
BannerController.java

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/test")
public class BannerController {

    private final BannerService bannerService;

    @GetMapping
    public ResponseEntity<SelectedBannersResponse> getBanners() {
        SelectedBannersResponse banners = bannerService.findSelectedBanners();
        return ResponseEntity.ok().body(banners);
    }
}

 
 
위 코드에 대한 테스트 코드이다.
실제 테스트 코드에서 일부 내용은 수정하였으나, 큰 틀은 동일하다.
 
 
BannerControllerTest.java

@AutoConfigureRestDocs
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@WebMvcTest(controllers = BannerController.class)
class BannerControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BannerService bannerService;

    @Test
    void 조회_정상_요청시_200_반환() throws Exception {
        given(bannerService.findSelectedBanners()).willReturn(Data);

        mockMvc.perform(get("/api/v1/test")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.banners[0].order", equalTo(1)))
                .andExpect(jsonPath("$.banners[0].title", containsString("...")))
                .andDo(print())
                .andDo(document("get banners",
                                preprocessRequest(prettyPrint()),
                                preprocessResponse(prettyPrint()),
                                resource(ResourceSnippetParameters.builder()
                                        .tag("Banner API")
                                        .summary("배너 조회 API")
                                        .description("description")
                                        .responseFields(
                                                fieldWithPath("banners").type(ARRAY).description("..."),
                                                fieldWithPath("banners[].title").type(STRING).description("..."),
                                                fieldWithPath("banners[].subTitle").type(STRING).description("...")
                                        )
                                        .build())
                        )
                );
    }
}

 
 
MockMvc + MockMvcRestDocumentationWrapper 를 이용해 테스트 코드를 작성한다.
기존 MockMvc + MockMvcRestDocumentation 와 작성법이 매우 유사해서 어렵지 않게 작성할 수 있다.
(참고로 본 글은 Rest Docs 를 사용하는 방법을 설명하는 글은 아니므로 Rest Docs 에 대한 설명은 넘어간다)

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;

 


 

API 문서화 결과 확인

지정한 URI로 이동하면 생성된 Swagger UI를 확인할 수 있다.

 
 
 


참고 자료

반응형