Spring Rest Docs, Swagger 로 API 명세서 자동 배포하기

치현·2025년 6월 3일

Spring Boot

목록 보기
3/4
post-thumbnail

API 명세서 자동 배포를 결정하기까지

기존에는 API 명세서로 노션을 활용했다. 노션의 데이터베이스 기능과 템플릿 기능을 이용해 손쉽게 명세서 작성을 위한 세팅을 할 수 있고, 적절한 태깅으로 문서 관리가 간편했다.

하지만, 점점 API 개수가 많아지면서 몇 가지 불편한 점이 있었다.

1) 오탈자 또는 타입 오기입 등의 휴먼 에러,

2) Swagger에서 제공하는 execute API tests 와 같은 기능의 부재,

3) 특정 도메인의 API 명세서를 보기 위해서 그때마다 필터링을 일일이 설정해야했다.


그래서 실제 서버와 명세서의 불일치로 클라이언트 또는 서버의 PR이 다시 작성되어야하는 경우가 종종 있었다. 그리고 원활한 협업을 위해서 API 명세서를 효율적으로 작성하고 볼 수 있는 방법을 고안해야했다.


결론적으로 Spring Rest Docs 를 사용해서 1번 문제를 해결하고, Swagger 를 사용해서 2번, 3번 문제를 해결했다.


Spring Rest Docs는 API 테스트 코드를 기반으로 문서(.adoc)를 생성하는 라이브러리이다. 이때 생성된 문서를 OpenAPI 표준 명세인 openapi3.yml 로 변환할 수 있다. 변환된 OpenAPI 표준 명세는 Swagger 에 연결해 흔히 아는 Swagger UI로 표현할 수 있다.


이 글은 콘페티 서버의 Spring Rest Docs와 Swagger를 연동하는 과정에 대해서 설명한다.


서버 환경 (버전 정보)

Spring Boot 3.4.1
JDK 21


Spring Rest Docs

테스트 코드를 기반으로 RESTful 문서 생성을 도와주는 도구이다.

테스트 코드를 기반으로 생성되기 때문에 비즈니스 로직이 포함된 제품 코드에 영향을 주지 않고 문서의 정확성과 안정성을 보장할 수 있다.


Spring Rest Docs를 보다보면 Asciidoctor라는 단어가 계속 등장한다. Asciidoctor는 AsciiDoc 문서 형식을 HTML 등 여러 형식으로 변환하는 Ruby 기반 텍스트 프로세서라고 한다. AsciiDoc는 사람이 읽을 수 있는 일반 텍스트 형식으로, 문서를 작성하기 위한 마크업 언어를 의미한다.

📍 장점

  • 제품 코드에 영향이 없다.
  • 문서의 안정성을 보장할 수 있다.
  • 커스텀이 자유롭다.

📍 단점

  • 설정이 까다롭고 레퍼런스가 적어 적용하기 어렵다.
  • 문서 스니펫 작성 후 테스트 코드를 작성하기 때문에 작성할 코드 양이 많아진다.
  • 생성되는 Asciidoc 파일의 UI가 단순하다.

Swagger

쉽게 API 명세서를 작성하고 효율적인 사용을 돕는 오픈소스이다.

📍 장점

  • 설정이 쉽다.
  • 생성된 문서에서 API 테스트가 가능하다.
  • API 문서가 자동으로 생성된다.

📍 단점

  • 제품 코드에 침투적이다.
  • 문서의 안정성을 보장하지 않는다.

RestAssured

API의 사용자 관점에서 테스트를 진행함으로써 API 안정성을 보장할 수 있는 Java DSL이다.

given-when-then 패턴으로 가독성이 좋고 블랙박스 테스트로 요청과 응답만 검증한다.


👉🏻 자세한 사용 방법은 공식문서를 참고

또는 아래의 블로그에 사용 방법이 정말 잘 설명되어있다.


Spring Rest Docs + Swagger 연동 과정

Spring Rest Docs의 장점과 Swagger의 장점을 모두 사용할 수 있도록 프로젝트를 구성해보자.


1️⃣ restdocs-api-spec 플러그인 추가

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'
    id 'io.spring.dependency-management' version '1.1.7'
+   id 'com.epages.restdocs-api-spec' version '0.18.2'
}

2️⃣ restdocs-api-spec, restassured 의존성 추가


dependencies {
    ... 다른 의존성
    // Spring Rest Docs
+   testImplementation "com.epages:restdocs-api-spec-restassured:0.18.2"
+   testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
+   testImplementation 'io.rest-assured:rest-assured'
+   testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
+   testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
}

3️⃣ openapi3 테스크 작성


openapi3 {
    servers = [
            { url = "https://api.confeti.xyz" },
            { url = "http://localhost:8080" }
    ]
    title = "CONFETI Server API Document"
    description = "응답 테스트 검증이 완료된 명세서입니다."
    version = "0.0.1"
    format = "yaml"
}

4️⃣ Restassured 및 API 스펙 설정

API 테스트는 문서화를 위한 코드이기 때문에 여러 클래스로 나뉘어 작성된다.

이때 코드의 중복을 피하기 위해 관련 설정은 최대한 추상화를 시켰다.

아래는 프로젝트에 적용된 APIBaseTest 추상 클래스이다.

package org.sopt.confeti.restdocs.base;

// 레퍼런스를 찾아볼 때 어디서 import 했는지 알면 도움이 많이 되었어서 핵심 import만 남겨보았다.import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris;import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
import org.sopt.confeti.global.exception.NotFoundException;
import org.sopt.confeti.global.message.ErrorMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) // Rest assured는 톰켓에서 제공하는 클라이언트 내장 서버를 통한 테스트를 진행하기 때문에 포트 충돌을 피하기 위해 랜덤 포트를 설정한다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class APIBaseTest {

    protected static final String DEFAULT_RESTDOC_PATH = "{class_name}/{method_name}/";
    protected RequestSpecification spec;

    @LocalServerPort
    int port;

    @BeforeEach
    void setUpRestDocs(RestDocumentationContextProvider provider) {
        RestAssured.port = port;

        this.spec = new RequestSpecBuilder()
                .setPort(port)
                .addFilter(documentationConfiguration(provider)
                        .operationPreprocessors()
                        .withRequestDefaults(
                                modifyUris().scheme("http").host("localhost").port(8080),
                                prettyPrint()
                        )
                        .withResponseDefaults(prettyPrint())
                )
                .build();
    }
}

5️⃣ 적용하기


public class Artist extends APIBaseTest {

    @Test
    @DisplayName("아티스트 검색 API 테스트")
    void 아티스트_검색_API() {
        given(this.spec)
                .filter(document(DEFAULT_RESTDOC_PATH,
                        queryParameters(
                                parameterWithName("accessToken").description("엑세스 토큰").optional(),
                                parameterWithName("term").description("검색어").optional(),
                                parameterWithName("aid").description("아티스트 아이디").optional()
                        ),
                        responseFields(
                                fieldWithPath("status").type(JsonFieldType.NUMBER).description("200"),
                                fieldWithPath("message").type(JsonFieldType.STRING).description("요청이 성공했습니다."),
                                fieldWithPath("data.artist").type(JsonFieldType.OBJECT).description("아티스트 객체"),
                                fieldWithPath("data.artist.artistId").type(JsonFieldType.STRING)
                                        .description("아티스트 아이디"),
                                fieldWithPath("data.artist.name").type(JsonFieldType.STRING).description("아티스트 이름"),
                                fieldWithPath("data.artist.profileUrl").type(JsonFieldType.STRING)
                                        .description("아티스트 프로필 URL"),
                                fieldWithPath("data.artist.recentAlbumName").type(JsonFieldType.STRING)
                                        .description("아티스트 최근 발매 앨범 제목"),
                                fieldWithPath("data.artist.isFavorite").type(JsonFieldType.BOOLEAN)
                                        .description("아티스트 좋아요 여부")
                        )
                ))
                .accept(MediaType.APPLICATION_JSON_VALUE)
                .header("Content-Type", "application/json")
                .when()
                .queryParam("term", "혁오")
                .get("/artists/search")
                .then()
                .statusCode(200);
    }

위에서 설명된 Restassured 문법을 참고해서 작성하면 된다.

가장 좋은 점은 문서에 작성된 조건과 실제 테스트에 사용되는 요청 값이 독립적이라는 것이다. 문서는 문서대로, 테스트는 테스트대로!


여기까지 구성하고 ./gradlew clean build 또는 build.gradle > openapi3 항목을 실행하면 build/api-spec 경로에 openapi3.yaml 파일이 생성된다.

테스트에 작성한 문서대로 생성되었는지 확인하고 다음으로 넘어가자.

6️⃣ Swagger 플러그인 추가


plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'com.epages.restdocs-api-spec' version '0.18.2'
+   id 'org.hidetake.swagger.generator' version '2.18.2'
}

7️⃣ Swagger 샘플 소스 추가


swaggerSources {
    sample {
        setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
    }
}

8️⃣ Swagger 의존성 추가


dependencies {
    ... 다른 의존성
    // Swagger
+   swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

9️⃣ Swagger 테스크 작성


import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI

tasks.withType(GenerateSwaggerUI) {
    dependsOn 'openapi3'

    doFirst {
        def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yml")

        def securitySchemesContent = "  securitySchemes:\n" +              \
                                                  "    bearerAuth:\n" +              \
                                                  "      type: https\n" +              \
                                                  "      scheme: bearer\n" +              \
                                                  "      bearerFormat: JWT\n" +              \
                                                  "      name: Authorization\n" +              \
                                                  "      in: header\n" +             \
                                                  "      description: \"Use 'your-access-token' as the value of the Authorization header\"\n" +             \
                                                  "security:\n" +
                "  - bearerAuth: []  # Apply the security scheme here"

        swaggerUIFile.append securitySchemesContent
    }
}

🔟 main/resources/static/docs에 생성된 Swagger UI 복사


// 생성된 Swagger UI 파일들 복사
tasks.register('copySwaggerDocument', Copy) {
    dependsOn generateSwaggerUISample

    from file("build/swagger-ui-sample/")
    into file("src/main/resources/static/docs")
}

bootJar {
    dependsOn copySwaggerDocument
}

모든 설정이 완료됐다.

이제 ./gradlew clean build 를 수행하면 테스트 이후 openapi3.yaml 파일이 생성되고 OpenAPI를 기반으로 Swagger UI가 생성된다.

그리고 서버를 실행하면 다음과 같은 화면을 볼 수 있다.



출처

profile
Backend Engineer

0개의 댓글