Skip to main content

restassured 와 restdocs에 swagger ui 곁들이기

· 14 min read
BE_로지

안녕하세요, 집사의고민 백엔드 개발자 로지입니다. 이번 글에서는 저희 팀이 선택한 API 문서화 방법을 소개하고 어떻게 적용했는지 알려드리려고해요.


웹 애플리케이션 API를 구현하고 관리하게 되면 문서화에 어떤 기술을 활용할지 고민하게되는데요, 스프링 진영에는 다음 기술들을 주로 고려하는 것 같습니다.

  • Spring Restdocs
  • Swagger Generator

결론적으로, 이 둘 중 하나를 써도 되지만 저희 팀은 두 기술을 모두 활용해보기로 했습니다.

저희 팀은 먼저, 실제로 호출되는 API와 문서가 동기화되는 restdocs를 사용하기로 결정했습니다. 하지만 기존에 restdocs를 사용해본 경험이 있는 크루들은 restdocs의 asscidoc 이 swagger ui에 비해 화면/기능에 있어 실용성이 부족하다고 생각했습니다.

이 부분을 절충하기 위해, swagger generator에서 ui를 생성하는 기능만을 사용해 restdocs와 함께 사용해보는 것으로 결정하게 되었습니다.

필요한 도구들

  • restassured (인수테스트 도구): 저희 팀은 인수테스트를 수행해 restdocs를 발행합니다. 인수테스트에 restassured라는 http 클라이언트 테스트 도구를 사용합니다.
  • restdocs (문서화도구): restassured 테스트를 진행하며 API의 정보를 adoc으로 발행합니다.
  • restdocs-api-spec: restdocs에서 만들어준 adoc으로는 바로 swagger UI를 만들 수 없습니다. swagger ui를 발행할 수 있도록, adoc을 open api spec으로 변환해주는 도구입니다.
  • gradle-swagger-generator-plugin : adoc에서 변환된 open api spec으로부터 swagger ui를 만드는데 사용할 도구입니다.

눈치채셨겠지만, 테스트부터 ui를 만드는 데까지 사용하는 순서대로 도구를 소개해드렸어요.

팀 개발 환경

저희 팀은 다음 환경에서 작업을 진행했습니다!

  • java 17
  • Spring boot 3
  • gradle 8.2.1
  • intellij idea

흐름 보기

자세한 설정까지는 아니더라도, 코드로 흐름을 본다면 이 글을 읽기가 더 수월할 것 같아 먼저 코드 조각들을 보여드리려고 합니다.

RestAssured로 테스트하는 코드

@Test
void 파라미터에_이름을_넣고_요청한다() {
// given
var 요청_준비 = given(spec)
.contentType(JSON)
.filter(성공_응답_문서_만들기("hello-rosie-world-성공"));

// when
var 응답 = 요청_준비.when()
.pathParam("name", "김크루")
.get("/rosie/{name}");

// then
응답.then()
.assertThat().statusCode(OK.value());
}

RestAssured 의 given, when, then 메서드를 사용해서 테스트를 진행합니다. 요청_준비 변수를 만드는 마지막줄에 filter 메서드에 RestDocs가 문서를 만들수 있도록 다음과 같이 명시해줍니다.

private RestDocumentationFilter 성공_응답_문서_만들기(String 제목) {
return document(제목,
API_정보.responseSchema(성공_응답_형식),
pathParameters(parameterWithName("name").description("놀러와주신 분의 성함")),
responseFields(
fieldWithPath("messages").description("환영의 단어들"),
fieldWithPath("ps").description("로지가 추가로 전하는 말").optional()
));
}

이 테스트를 실행하면, restdocs가 adoc을 만들어줍니다.

open api 스펙으로 변환하기

만들어진 adoc 파일은 다음과 같은데요, swagger UI에 활용할수 있도록 openapi 형식으로 변환해줍니다. openapi3 태스크를 실행하면 open api 3.0 스펙으로 생성돼요.

이렇게 만든 문서는 다음 화면에 대응될 예정입니다 :D

build.gradle 설정

제가 사용한 그래들 스크립트입니다.

플러그인

plugins {
// 2. restdocs-api-spec 플러그인 추가
id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"

// 3. swagger-generator 플러그인 추가
id 'org.hidetake.swagger.generator' version '2.18.2'
}

dependencies

dependencies {
testImplementation 'io.rest-assured:rest-assured'
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testImplementation "com.epages:restdocs-api-spec-restassured:${restdocsApiSpecVersion}"
testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"

swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

플러그인/태스크 설정

  • openapi3 설정
  • 스웨거 ui 생성
  • bootJar 설정

openapi3

우선 restdocs-api-spec 의 설정을 해줄거예요.

openapi3 { // 4. open api 설정
setServer("http://localhost:8080") // 4-1. 요청 보낼 서버의 baseUrl
title = "로지 API Docs" // 4-2. 제목
description = "로지네 API 명세서" // 4-3. 설명
version = "0.0.1" // 4-4. api 문서의 버전
format = "yaml" // 4-5. openapi 형식을 저장할 형식 (yaml/json)
}

코드와 대응하는 화면을 보여드릴게요.

GenerateSwaggerUI 설정

그리고 나서, swaggerUI 를 만드는 태스크를 정의해줍니다. 자세한 설명은 주석을 참고해주세요.

tasks.withType(GenerateSwaggerUI) { // 5. swaggerUI 를 생성하는 task들은 전부
dependsOn 'openapi3' // openapi3 이 실행된 이후에 실행되도록 설정
}

swaggerSources { // 6. swagger ui 생성에 필요한 source 설정.
rosieProject { // ⭐️ rosieProject(이름은 프로젝트에 맞도록 변경) 라는 스웨거 ui의
setInputFile(file("build/api-spec/openapi3.yaml")) // 소스 위치를 설정. (현재는 default 위치)
}
}

// 7. rosieProject 에 대한 swagger ui를 생성하는 task.
generateSwaggerUIRosieProject {
doLast { // ui 파일들이 생성되고 난 뒤
copy { // 정적 리소스 디렉토리로 복사
from outputDir.toPath()
into "build/resources/main/static/docs" // jar 파일에 포한되려면 build/ 내의 위치로 설정해야합니다!
}
}
}

[2023.08.05 수정]

이전에 제가 실수로 src 하위에 (src/main/resources/static/docs) 파일을 복사하도록 했는데, 이렇게하면 jar 패키징에 생성한 ui파일들이 포함되지 않습니다~

bootJar 태스크 설정

마지막으로, jar 패키징에 swagger ui가 포함될 수 있도록 설정해줍니다.

bootJar {
dependsOn generateSwaggerUI
}

만약 위 스크립트로 안되면

bootJar {
dependsOn tasks.withType(GenerateSwaggerUI)
}

이렇게 해보시면 됩니다.

테스트 코드 작성

RequestSpecification 설정

RestAssured에서 Restdocs를 사용하기 위해서, 저는 다음과 같이 추상클래스에서 RequestSpecification 필드를 관리하게 되었습니다.

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class AcceptanceTest {

protected RequestSpecification spec;

@LocalServerPort
private int port;

@BeforeEach
void setUp() {
RestAssured.port = port;
}

@BeforeEach
void setUpRestDocs(RestDocumentationContextProvider provider) {
RestAssuredOperationPreprocessorsConfigurer filter = documentationConfiguration(provider)
.operationPreprocessors();

this.spec = new RequestSpecBuilder()
.setPort(port)
.addFilter(filter)
.build();
}

}

restdocs가 설정된 RequestSpecification 사용하기

restAssured 사용시, specification을 명시하지 않는 given() 을 이용하면 다음과 같이 매번 새로운 Specification을 사용합니다.

우리가 위에서 설정해준 spec 변수를 이용하기 위해서는 다음 방법으로 restAsscured를 사용해야합니다.

(1) given(spec)
(2) given().spec(spec)
(3) given을 사용하지 않는경우: when().spec(spec)

테스트 별로 생성할 문서 정의하기

보통 restdocs를 asciidoc 으로 문서를 생성할 때는, 다음과 같이RestAssuredRestDocumentation.document 를 사용하는데요,

이 메서드를 restdocs-api-spec이 제공하는 wrapper 클래스의 메서드로 바꿔 사용합니다. (구현을 보면 많이 다를 건 없지만, 깃허브를 보면 openapi spec으로 변환할 때 편리함을 위해 제공한다고 합니다.)

private ResourceSnippetDetails API_정보 = resourceDetails()
.summary("로지월드 들어가기 API")
.description("로지 월드에 와서 환영을 받습니다.");

@Test
void 파라미터에_이름을_넣고_요청한다() {
// given
var 요청_준비 = given(spec)
.contentType(JSON)
.filter( // 여기입니다.
RestAssuredRestDocumentationWrapper.document("hello-rosie-world-성공", API_정보)
);

// when
var 응답 = 요청_준비.when()
.pathParam("name", "김크루")
.get("/rosie/{name}");

// then
응답.then()
.assertThat().statusCode(OK.value());
}

같은 API에 대한 여러 요청(응답)

같은 API에 대해서도, 여러 응답을 문서화해야하는데요. 이 작업은 별도의 작업이 필요하진 않습니다. 테스트별 document 메서드의 ResourceSnippetDetails 인자를 동일하게 설정해주면, 하나의 요청으로 간주하여 다음과같이 응답을 합쳐줍니다.

document() 사용법

restdocs-api-spec 모듈은 코틀린으로 작성되어서.. 참고하시며 작업하시길 바라구요. 어쨌든, document() 메서드에는 많은 인자들이 들어갈 수 있는데요. 각 인자가 어떤 요소에 대응되는지 정리해봤습니다.

identifier: rest docs 생성 파일 디렉토리명

document() 메서드의 첫번째 인자인데요, 이 아이는 restdocs에서 코드를 생성할 때 만들어지는 디렉토리의 이름이됩니다.

given(spec)
.contentType(JSON)
.filter(document("hello-rosie-world-성공", API_정보));

이렇게 설정하고, 테스트를 통과한다면 설정한 이름의 디렉토리에 adoc 파일들이 생성된 것을 확인할 수 있어요.

resourceDetails : API 정보

API 정보는 ResourceSnippetDetails 클래스 로 정의합니다.

ResourceSnippetDefails API_정보 = resourceDetails()
.summary("로지월드 들어가기 API")
.description("로지 월드에 와서 환영을 받습니다.")

given(spec)
.contentType(JSON)
.filter(document("hello-rosie-world-성공", API_정보));

이 코드는 다음 화면에 대응돼요.

requestProcessor , responseProcessor: 요청/응답에 적용할 프로세서

OperationProcessor 객체를 인자로 받습니다. OperationProcessor는 요청 또는 응답이 문서화되기 전에 적용하고 싶은 것을 정의할 수 있어요.

+) 프로세서는 다음과 같은 것들이 있어요: prettyPrint(), removeHeaders() 등..

snippeetFilters

Function<List<Snippet>, List<Snippet>> 인데요, 변수명과 자료형을 보아 스니펫 리스트를 필터링하는데 사용하는 것 같습니다.

그러나 정확히 어떤 목적을 위한 변수인지는 모르겠습니다! 아시는 분은 알려주세요~ 저는 쓸 필요를 못느껴서 Pass~

snippets : 요청/응답 예시 등 API 문서 조각에 쓰이는 모든 것

org.springframework.restdocs.snippet 에 정의돼있는 Snippet 객체를 가변 인자로 받아요.

저는 보통 다음과같이 요청 파라미터, 요청/응답 바디 Snippet을 활용하는 편입니다.

document("hello-rosie-world-성공",
API_정보.responseSchema(성공_응답_형식),
// 파라미터에 대한 snippet 생성
pathParameters(parameterWithName("name").description("놀러와주신 분의 성함")),
// 응답 바디에 대한 snippet 생성
responseFields(
fieldWithPath("messages").description("환영의 단어들"),
fieldWithPath("ps").description("로지가 추가로 전하는 말").optional()
));

이외에도 Snippet은 여러가지 종류가 있는데요, 스니펫을 만들기 위해서는 지정된 정적 팩토리 메서드를 사용해야해서, 문서화가 필요한 대상의 키워드(ex. 쿼리파라미터, 헤더)로 restdocs 공식문서에서 검색해보시면 쉽게 찾을 수 있습니다 :D

전체 코드

전체 코드는 깃허브에 올려뒀습니다. 글에서 조각으로 보면 복잡한 것 같기도한데, 한번에 보면 괜찮은 것 같아요.

https://github.com/kyY00n/restassured-restdocs-swaggerui


꽤 긴 튜토리얼이었는데요, 처음부터 꼼꼼히 읽기 보다는 초반에 소개드렸던 적용 흐름을 파악하시고, 디테일한 부분들은 공식문서와 함께 보는 것을 추천합니다 :D

여튼 읽어 주셔서 감사합니다. 올바르지 않거나 부족한 설명이 있다면 알려주세요! 여러분의 피드백을 격하게 환영합니다 :D

References