Spring REST Docs로 API 문서화하기 - woowa-turkey/miniprojects-2019 GitHub Wiki

참고: http://woowabros.github.io/experience/2018/12/28/spring-rest-docs.html

참고 2: https://spring.io/guides/gs/testing-restdocs/

Spring REST Docs는 Spring 프로젝트 중 API 문서를 만들도록 지원하는 프로젝트이다. 테스트 코드를 활용하여 문서화한다는 점이 큰 특징이다.

REST Docs로 문서화할 때 Spring Web 테스트 객체인 MockMvc, RestAssured, WebTestClient 모두 문서화 할 수 있도록 지원한다. 이번 프로젝트에서는 WebTestClient를 기반으로 테스트를 작성하였으므로 WebTestClient를 통한 API 문서화를 알아본다. (사실 사용법은 모두 같음)

기본적으로 Spring REST Docs를 사용하려면 Java 8 이상, Spring 5 이상이여야한다.

Setting

우리 프로젝트에서는 JUnit5를 통해 테스트를 진행하며 웹 테스트 객체로는 WebTestClient를 사용한다. 따라서 각각의 세팅이 필요하다.

Gradle Setting

// build.gradle

plugins { 
	id "org.asciidoctor.convert" version "1.5.3"
	// Ascii docs를 만들기 위한 plugin 등록
}

dependencies {
	asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.3.RELEASE'
	// snippets를 자동으로 추가하는 설정. ext.snippetsDir로 설정한 경로에 .adoc 파일을 만들어준다.
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.3.RELEASE'
	// test에서 rest docs를 만들기 위한 메서드 등을 가져오는 의존성
        testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
        // WebTestClient 사용을 위한 설정
        testImplementation 'org.junit.jupiter:junit-jupiter-api'
        // JUnit5 사용 설정
        testImplementation('org.springframework.boot:spring-boot-starter-test') {
            exclude group: 'junit'
        } // 기존의 Spring boot starter test 의존성에서 junit4 제거
}

ext { 
	snippetsDir = file('build/generated-snippets')
	// snippets를 만들 경로 설정
}

test { 
        useJUnitPlatform()
	outputs.dir snippetsDir
	// test를 실행하면 snippet을 만들도록 설정
}

asciidoctor { // asciidoctor task 설정
	inputs.dir snippetsDir
	// .adoc으로 만든 문서를 html으로 만들기 위해 .adoc 파일의 위치 설정
	dependsOn test
	// test task가 끝난 후 asciidoctor가 실행되도록 설정
}

bootJar {
	// 만든 rest docs를 jar 파일 내부로 넣기 위한 설정
	// Spring boot를 패키징할 때 만들어진 rest docs를 static/docs로 이동시켜준다.
	dependsOn asciidoctor 
	from ("${asciidoctor.outputDir}/html5") { 
		into 'static/docs'
	}
}

참고로 공식문서는 JDK 8을 기반으로 설명하고 있다. JDK 8에서는 org.asciidoctor.convert 버전이 1.5.3이여도 충분하지만 그 이상의 JDK에서는 위 convert 플러그인으로는 다음과 같은 오류가 난다.

캡처

그 이상의 버전에서는 최근에 나온 1.5.7버전을 사용해야하는 듯 하다.

WebTestClient Setting

@SpringBootTest
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class SampleJUnit5ApplicationTests {

private WebTestClient webTestClient;

@BeforeEach
public void setUp(WebApplicationContext webApplicationContext,
		RestDocumentationContextProvider restDocumentation) {
	this.webTestClient = WebTestClient.bindToApplicationContext(webApplicationContext)
			.configureClient()
			.filter(documentationConfiguration(restDocumentation)) 
			.build();
}

// ...
}

위 예시는 공식문서에서 소개된 WebTestClient를 문서화 할 수 있도록 설정하는 부분이다. 공식문서에는 WebTestClient를 사용하는 경우는 서버가 WebFlux를 사용한다고 가정하는듯 한다. 따라서 위 설정은 WebFlux를 사용하는 경우에만 사용해야한다.

※ 위 설정대로 우리 프로젝트에서 문서화 설정을 하면 나타나는 오류

webHandler 오류

위 설정으로는 Spring MVC를 사용하는 우리 프로젝트에서는 webHandler를 찾을 수 없다는 오류가 나타난다.

우리 프로젝트의 경우는 위 상황과 다르다. 테스트는 WebTestClient를 사용하지만 Spring MVC를 사용하고 있기 때문이다. 이 경우에는 띄워져 있는 서버를 직접 바인딩하여 사용해야한다.

@SpringBootTest
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class SampleJUnit5ApplicationTests {

private WebTestClient webTestClient;

@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
        webTestClient = WebTestClient.bindToServer()
                .filter(documentationConfiguration(restDocumentation)
                        .operationPreprocessors()
                        .withRequestDefaults(prettyPrint())
                        .withResponseDefaults(prettyPrint()))
                .build();
}

// ...

위와 같이 bindToServer로 직접 띄워져 있는 서버에 바인딩을 해주어야한다. 기본 주소는 http://localhost:8080이며 baseUrl 메서드로 주소를 변경할 수 있다.

 webTestClient = WebTestClient.bindToServer().baseUrl("https://www.naver.com");

// ...

위 예시는 webTestClient가 naver에 바인딩 될 것이다.

API 문서화

Spring REST Docs에서는 요청 Parameter, Path Parameter 요청/응답 헤더, 요청/응답 fields (application/json), 요청parts (multipart/form-data) 등에 대한 문서화를 제공한다.

Basic

기본적으로 document() 메서드를 통해 요청에 대한 문서화를 할 수 있다.

this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON) 
		.exchange().expectStatus().isOk() 
		.expectBody().consumeWith(document("index"));

WebTestClient에서는 위 예시처럼 consumeWith 메서드의 인자로 document()를 제공한다. document메서드의 첫번째 인자는 문서화 할 snippets이 담길 폴더의 이름이며 두번째 인자는 Snippets이 들어온다.

위 예시처럼 document를 index로 문서화하면 다음 6가지 snippets이 생성된다.

  • <output-directory>/index/curl-request.adoc
  • <output-directory>/index/http-request.adoc
  • <output-directory>/index/http-response.adoc
  • <output-directory>/index/httpie-request.adoc
  • <output-directory>/index/request-body.adoc
  • <output-directory>/index/response-body.adoc

기본적으로 curl-request.adoc, http-request.adoc, http-response.adoc, httpie-request.adoc, request-body.adoc, response-body.adoc의 6가지 문서가 생성된다. 그 외 다른 문서들은 문서화하기에 따라서 추가로 생성될 수 있다.

링크 문서화는 우리 프로젝트에서는 사용하지 않았으므로 따로 작성하지 않음. 링크 문서화는 아래 링크를 참조

https://docs.spring.io/spring-restdocs/docs/2.0.3.RELEASE/reference/html5/#documenting-your-api-hypermedia

Path Parameter

PathVariable로 받는 URI 변수를 Path Parameter라고 하며 이에 대한 문서화 pathParameters() 메서드로 지원한다.

// ...
        PostResponse postResponse = webTestClient.get().uri(POST_URL + "/{postId}", postId)
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .consumeWith(document("post",
                        pathParameters(
                                parameterWithName("postId").description("수정할 Post의 id")
                        ),
                 // ...
                )
// ...

위 예시처럼 document의 두번째 인자로 pathParameters를 사용하여 문서화 할 수 있다. pathParameters의 인자는 parameterWithName()을 사용한다.

주의점

pathParameters에서 오류가 많이 났던 부분은 parameterWithName의 인자가 실제 컨트롤러에서 설정한 id값을 사용해야한다고 생각했기 때문이다. pathParameters는 현재 테스트하고 있는 uri에서 설정한 id 변수명을 사용해서 문서화해야한다.

Request Parameter

form 데이터를 보내거나 쿼리스트링을 포함하여 요청하는 경우 요청 파라미터도 같이 전달한다. 이 부분에 대한 문서화도 Spring REST Docs에서 requestParameters로 지원한다.

requestParameters(
    parameterWithName("page").description("페이지 번호")
),
// ...

pathParameters와 마찬가지로 parameterWithName 메서드로 문서화를 지원한다.

Request / Response Payload (MediaType.APPLICATION_JSON)

Json 형식으로 요청을 주고 받는 경우는 각각 requestFields, respnseFields 메서드로 문서화해야한다.

document("post/create", responseFields(
            fieldWithPath("id").description("글의 고유 id"),
            fieldWithPath("contents.contents").description("글의 내용"),
            fieldWithPath("createdAt").description("글 작성 일자"),
            fieldWithPath("updatedAt").description("글 수정 일자"),
            fieldWithPath("totalComment").description("글에 달린 댓글의 총 갯수"))
)
// ...

requestFields와 responseFields는 fieldWithPath 메서드로 Json의 각 필드를 문서화할 수 있다.

Request도 마찬가지로 fieldWithPath로 문서화를 한다.

request Parts

multipart/form-data로 요청을 보내는 경우는 requestFields로 문서화할 수 없다. 이 경우는 requestParts 메서드로 문서화해야한다.

requestParts(
    partWithName("contents").description("글의 내용"),
    partWithName("files").optional().description("글과 함께 업로드하는 사진 또는 동영상, 여러장 가능"),
    partWithName("receiver").optional().description("다른 유저에게 글쓰는 경우, 해당 유저의 id")
),
// ...

requestParts에서는 partWithName으로 각 필드를 문서화한다.

Relaxed Mode

현재까지 알아본 문서화 메서드는 사용했을 때 문서화하지 않은 필드가 있거나 실제 필드에 없는 필드를 문서화한 경우 테스트가 깨져버린다. 이때 전자의 문제를 막을 수 있는 방법을 제공해준다. 바로 Relaxed Mode를 사용하는 것이다.

링크, 파라미터, Json 형식에서 필드, multipart에서 part에 대해 실제로 존재하는 필드에 문서화를 하지 않아도 되는 모드를 relax mode라고 한다. 단, 없는 필드를 문서화하는 경우는 테스트가 실패하게 된다.

생성한 Snippets으로 Html 문서 만들기

Gradle에서는 src/docs/*.adoc 형식으로 문서화할 수 있다. 원하는 이름의 .adoc을 만들면 해당 이름의 Html로 변환된다.

만약 index.adoc으로 만들면 index.html이 만들어지고 api-guide.adoc으로 만들면 api-guide.html이 만들어진다. Spring Boot에서는 정적자원에 대한 접근을 제공해주므로 각각 /docs/index.html/docs/api-guide.html로 접근할 수 있다.

asciidoc 사용법: https://narusas.github.io/2018/03/21/Asciidoc-basic.html

참고: https://jojoldu.tistory.com/294