-
Notifications
You must be signed in to change notification settings - Fork 1
Spring REST Docs를 배워서 남주자
Spring REST Docs는 RESTful 서비스에 대한 정확하고 읽기 쉬운 문서를 만들어주는데 도움을 주는 것을 목표로한다.
높은 수준의 문서를 작성하는 일은 매우 어렵다. 이런 어려운 작업을 쉽게 해주는 방법 중 하나는 그 일에 매우 적합한 도구를 사용하는 것이다. 이를 위해 Spring REST Docs는 Asciidoctor를 기본값으로 사용한다. Asciidoctor는 명백한 글자를 처리하고 HTML을 만들어주며 여러 요구사항에 따른 스타일링과 레이아웃을 나눠준다. 만약 Markdown이 편하다면 언제든지 설정 사항을 변경하면 된다.
Spring REST Docs는 snippet을 사용하는데 이는 Spring MVC's framework, Spring WebFlux's WebTestClient 또는 REST Assured 3로 작성된 테스트 코드에 의해 생성된다. 이런 테스트 기반 접근은 서비스의 문서화를 보다 정확하게 만들어줌을 보장한다. 만약 snippet이 부정확하다면 이를 생성한 테스트는 깨질 것이다.
RESTful 서비스를 문서화하는 것은 여러 자원을 명시하는 것과 매우 연관이 있다. 각 자원에 대한 명세의 두가지 주요 부분이 있는데 해당 자원들이 사용하는 HTTP 요청과 생성하는 HTTP 응답에 대한 세부 사항들이다. Spring REST Docs는 이런 자원들과 HTTP 요청, 응답과 함께 작업할 수 있도록 해준다. 동시에 서비스 구현 로직의 내부 세부 사항에 대한 문서로 독립적으로 유지시켜준다. 이런 분리는 구현 로직보단 서비스의 API를 문서화하는데 집중하게 해주며 또한 문서화의 재작업 없이 자유롭게 구현 로직을 발전시켜나갈 수 있게 해준다.
@SpringBootTest
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class SampleJUnit5ApplicationTests {
private WebTestClient webTestClient;
@BeforeEach
public void setUp(RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient.bindToServer()
.filter(documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint()))
.build();
}
this.webTestClient.get().uri("/").accept(MediaType.APPLICATION_JSON)
.exchange().expectStatus().isOk()
.expectBody().consumeWith(document("index"));
위 로직은 'index'라는 이름의 디렉토리안에 snippet을 작성하면서 서비스 콜에 대한 문서화 작업을 진행한다.
snippet은 ExchangeResult
의 Consumer
에 의해 작성된다.
this.webTestClient.get().uri("/users?page=2&per_page=100")
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("users", requestParameters(
parameterWithName("page").description("The page to retrieve"),
parameterWithName("per_page").description("Entries per page")
)));
this.webTestClient.get().uri("/locations/{latitude}/{longitude}", 51.5072, 0.1275)
.exchange().expectStatus().isOk().expectBody()
.consumeWith(document("locations",
pathParameters(
parameterWithName("latitude").description("The location's latitude"),
parameterWithName("longitude").description("The location's longitude"))));
curl-request
http-request
http-response
httpie-request
request-body
response-body
그외 문서화 작업 수행 메서드에 이름에 맞춰 snippet이 생성된다.
위 Request Parameters, Path Parameters의 경우
request_parameters.adoc
, path_parameters.adoc
가 생성된다.
이렇게 생성된 snippet을 최종 문서로 만들기 위해
문서화 모듈 경로 src/main/docs/asciidoc
에 XXX.adoc 파일을 생성한다.
include::{snippets}/example/curl-request.adoc[]
include::{snippets}/example/request-parameters.adoc[]
include::{snippets}/example/http-request.adoc[]
include::{snippets}/example/response-fields.adoc[]
include::{snippets}/example/http-response.adoc[]
include::share-content.adoc[] 오픈 블럭을 이용해 다른 문서를 끼워넣는 asciidoc 문법이다.
Spring REST Docs 에서 asciidoc를 사용하는 가장 큰 이유다. -> 다양한 문서를 쉽게 임포트할 수 있다는 장점
build.gradle
plugins { // 1
id "org.asciidoctor.convert" version "1.5.9.2"
}
ext { // 2
set('snippetsDir', file("build/generated-snippets"))
}
test { // 3
outputs.dir snippetsDir
}
asciidoctor { // 4
inputs.dir snippetsDir // 4-1
dependsOn test // 4-2
}
bootJar { // 5
dependsOn asciidoctor // 5-1
from ("${asciidoctor.outputDir}/html5") { // 5-2
into 'static/docs'
}
}
dependencies { // 6
asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-webtestclient'
}
-
Asciidoctor plugin 적용
-
snippetsDir
라는 프로퍼티에 생성될 snippets의 위치 정의 -
test task 수행 결과로 나올 snippets 디렉토리 설정
-
asciidoctor task 설정
- snippets 디렉토리을 입력 디렉토리로 설정
- test task에 의존적이게 설정 → asciidoctor 테스크가 수행되기전에 test task가 수행되도록 하기 위함
-
bootJar task 설정
- asciidoctor task에 의존적이게 설정 → 위와 동일
- asciidoctor task의 결과물(문서)이 build/asciidoc/html5 디렉토리에 위치하는데 그곳에 있는 파일을 static/docs로 복사
-
spring-restdocs-asciidoctor
의존성 추가 /spring-restdocs-webtestclient
의존성 추가만약
webtestclient
를 사용하지 않고 mockmvc나 restassured를 사용한다면 그에 맞는 의존성을 추가해주면 된다.
test/java/com/geajangmo/apiserver/model/product/controller/ProductAcceptanceTest.java
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) // 1
public class ProductAcceptanceTest {
private WebTestClient webTestClient;
@BeforeEach
public void setUp(ApplicationContext applicationContext,
RestDocumentationContextProvider restDocumentation) {
this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext)
.configureClient()
.filter(documentationConfiguration(restDocumentation)) // 2
.build();
}
...
}
- JUnit5 에서 snippets을 생성하기 위한 설정 어노테이션 추가
-
WebTestClient
인스턴스를 이용하여 API 문서를 생성하기 위해 자동화 문서 필터 추가
그러나 이는 Spring Web Flux Framework 기준에 맞는 설정이다. Spring MVC Framework를 사용하고 있다면 설정을 다르게 해줘야한다.
testjava/com/geajangmo/apiserver/model/product/controller/ProductAcceptanceTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class ProductAcceptanceTest {
@Autowired
private WebTestClient webTestClient;
@LocalServerPort
private String port;
@BeforeEach
void setUp(RestDocumentationContextProvider restDocumentation) {
webTestClient = WebTestClient.bindToServer() // 1
.baseUrl("http://localhost:" + port)
.filter(documentationConfiguration(restDocumentation))
.build();
}
...
}
- Spring MVC Framework를 사용하고 있으면서 WebTestClient로 테스트를 수행하고 있다면
bindToServer()
메서드로 현재 돌아가고 있는 서버에 연결시켜준다.
test/java/com/geajangmo/apiserver/model/product/controller/ProductAcceptanceTest.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class ProductAcceptanceTest {
...
@Test
void 장비조회() {
ProductResponseDto productResponseDto = webTestClient.get()
.uri(uriBuilder ->
uriBuilder.path(PRODUCT_API)
.queryParam("productName", "애플 맥북 프로 15형 2019년형 MV912KH/A")
.build())
.exchange()
.expectStatus().isOk()
.expectBody(ProductResponseDto.class)
.consumeWith(document("post", // 1
requestParameters( // 2
parameterWithName("productName").description("찾으려는 Product의 이름") // 2-1
)))
.returnResult()
.getResponseBody();
assertThat(productResponseDto).isEqualTo(ProductTestData.RESPONSE_DTO);
}
...
}
-
document()
메서드는 첫번째 인자로 갖고 있는 "post"는 그 이름의 디렉토리로 snippets을 그리게 된다. -
requestParameters()
는 Spring Rest Docs가 requests' parameters를 명세하는 snippet을 생성하게 설정한다.-
parameterWithName()
메서드는 인자로 들어오는 "productName"을 문서화하고description()
메서드로 그에 대한 설명을 명세한다.
여기서 "productName"는 실제 프로덕션 코드에서 인자로 받기위해 선언된 파리미터 이름과 일치해야한다.
-
main/java/com/gaejangmo/apiserver/model/product/controller/ProductApiController.java
@RestController
@AllArgsConstructor
@RequestMapping("/api/v1/products")
public class ProductApiController {
private final ProductService productService;
@GetMapping
public ResponseEntity<ProductResponseDto> find(@RequestParam(name = "productName") String name) {
ProductResponseDto productResponseDto = productService.findByProductName(name);
return ResponseEntity.ok(productResponseDto);
}
}
위 find() 메서드는 @Test 장비조회()
테스트 코드가 테스트하는 프로덕션 코드다.
여기서 중요한 점은 find() 메서드의 파리미터 이름으로 name이 선언되어 있지만 실제 받는 값은 @RequestParam(name = "productName")
이기 때문에 "productName" 이름으로 받는다.
그렇기 때문에 테스트 코드에서 문서화 작업 시 parameterWithName()
메서드의 인자로 "productName"을 넣어줘야한다.
requestParameters(
parameterWithName("productName").description("찾으려는 Product의 이름")
)
build.gradle
ext {
set('snippetsDir', file("build/generated-snippets")) // 정해진 경로
}
build/generated-snippets/post
기본으로 생성되는 snippets (6개) 외 request-parameters.adoc
이 생성된 것을 볼 수 있다.
이처럼 기본으로 제공되는 snippet외 snippet을 명세하는 메서드를 문서화 작업 코드에 추가하면 새롭게 snippet이 생성된다.
.consumeWith(document("post",
requestParameters(
parameterWithName("productName").description("찾으려는 Product의 이름")
)))
그외 snippet을 생성하는 방식(request fields, response fields, path parameters, request body, response body 등)은 레퍼런스로 찾아보자.
build/generated-snippets/post/request-parameters.adoc
src/docs/asciidoc/index.adoc
= Getting Started With Spring REST Docs
This is an example output for a service running at http://localhost:8080:
== Post
.request
include::{snippets}/post/http-request.adoc[]
include::{snippets}/post/request-parameters.adoc[] // 1
.response
include::{snippets}/post/http-response.adoc[]
-
이렇게 하나의 adoc 문서안에 위에서 여러개 생성된 snippets을
include::
라는 명령어로 import해올 수 있다.- 위 문서는
http-request.adoc
,request-parameters.adoc
,http-response.adoc
이렇게 3개의 snippets을 import 하고 있다. - 이러한 통합 문서의 위치를 src/docs/asciidoc 으로 위치시키면 build 시 build/asciidoc/html5/ 경로에
index.html
파일이 생성된다. - 생성되는 html 파일의 이름은 통합 문서에 해당하는 ***.adoc에 파일 이름을 그대로 따라 만들어진다. index.adoc → index.html
- 위 문서는
-
그리고 gradle 스크립트로 설정했던 bootJar task에 의해 static/docs/ 경로에 이 build/asciidoc/html5/index.html 파일이 복사된다.
물론 build를 성공했어도 static/docs 경로에는 index.html이 안보인다. 이는 오류가 아니라 jar 파일로 프로젝트가 패키징될 때 해당 경로에 index.html이 잘 복사돼서 들어간 것이다. 이를 확인하기 위해 jar 파일을 실행시키고 브라우저에서 확인해보자.
localhost:8080/docs/index.html 로 들어가면 위와 같이 Rest Docs가 나오는 것을 확인할 수 있다.
이 문서는 src/docs/asciidoc/index.adoc을 asciidoctor task 가 변환하여 index.html로 만들어준 문서다.
-
[회의록]
-
[데일리]
-
[기술 & 이슈 정리]
-
[TIPS]