Spring REST Docs ์ ์ฉ (Gradle 7)
1. Spring REST Docs
์๋ ํ์ธ์, ์ผ๋น์ ๋๋ค. Pick-Git ์๋น์ค ๊ฐ๋ฐ ์ด๊ธฐ์๋ ํ๋ก ํธ์๋ ํ์๋ค๊ณผ API ์คํ ํ์ ํ Notion์ ์์ฑํด์ ๊ณต์ ํ์๋๋ฐ์. ํด๋น ๋ฐฉ๋ฒ์๋ ์ฌ๋ฌ ๋ฌธ์ ์ ์ด ์กด์ฌํ์ต๋๋ค. ์๊ธฐ๋ก ์์ฑํ๋ค ๋ณด๋ ์ธ์ ์ค์๋ก ์ธํด ์ค๊ธฐ์ฌ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์ต๋๋ค. ๋ํ ๊ฐ๋ฐ ๊ณผ์ ์์ API ์คํ ๋ณ๊ฒฝ์ด ๋งค์ฐ ๋น๋ฒํ๊ฒ ๋ฐ์ํ๋๋ฐ, ์ด๋ฅผ ๋ฏธ๊ธฐ์ฌํ๋ ์ค์๋ก ์ธํด ์ผ์ ์ ์ฐจ์ง์ด ์ข ์ข ์๊ธฐ๊ณค ํ์ต๋๋ค.
ํ์ ๋ชจ๋ ์กฐ๊ธ ๋ ๊ท๊ฒฉํ๋ API ๋ฌธ์ํ์ ๋์ ์ ํ์์ฑ์ ๋๊ผ์ต๋๋ค. ์ฐ๋ฆฌ ํ์ด ์ ํํ ๋๊ตฌ๋ Spring Rest Docs์ ๋๋ค. Spring REST Docs๋ RESTful ์๋น์ค์ ๋ฌธ์ํ๋ฅผ ๋์์ฃผ๋ ๋๊ตฌ์ธ๋ฐ์. ๋ฌธ์ ์์ฑ ๋๊ตฌ๋ก ๊ธฐ๋ณธ์ ์ผ๋ก Asciidoctor๋ฅผ ์ฌ์ฉํ๊ณ , ์ด๋ฅผ ํตํด ์ต์ข ์ ์ผ๋ก HTML์ ์์ฑํฉ๋๋ค. ์ฌ์ง์ด Markdown์ ์ฌ์ฉํ๋๋ก ์ค์ ํ ์ ์์ต๋๋ค.
์ ๋ฐ์ ์ธ ํ๋ฆ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Spring MVC Test, Spring WebFlux WebTestClient, RestAssured ๋ฑ์ผ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ค.
- ํด๋น ํ ์คํธ ์ฝ๋๋ฅผ ํตํด Snippet์ด ์๋ ์์ฑ๋๋ค.
- ์ฌ์ฉ์๊ฐ ๋ฏธ๋ฆฌ ์์ฑํด๋ ํ ํ๋ฆฟ ๋ฌธ์์ Snippet์ ๊ฒฐํฉํด ์ต์ข ์ ์ธ API ๋ฌธ์๋ฅผ ๋ง๋ค์ด๋ธ๋ค.
1.1. Swagger ๋๋น ์ฅ์
๋น์ทํ ๋์์ผ๋ก Swagger๊ฐ ์์ต๋๋ค. ์ด ๋ํ ์ฝ๊ฒ API๋ฅผ ๋ฌธ์ํํ ์ ์๋๋ฐ์. ํนํ Swagger๋ API ๋์์ ํ ์คํธํ๋๋ฐ ํนํ๋์์ต๋๋ค. ๊ทธ๋ฌ๋ Swagger๋ ํ๋ก๋์ ์ฝ๋์ ์ ๋ํ ์ด์ ์ ๋ถ์ฐฉํ๋ ๋ฑ ์ฝ๋ ์นจํฌ๋ผ๋ ๋จ์ ์ด ์กด์ฌํฉ๋๋ค.
๋ฐ๋ฉด Spring REST Docs๋ ํ๋ก๋์ ์ฝ๋์ ๋ณ๋ค๋ฅธ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค. ์์ธ๋ฌ ํ ์คํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋๋ฉฐ, Snippet์ด ์ฌ๋ฐ๋ฅด์ง ์์ ๊ฒฝ์ฐ ํ ์คํธ๊ฐ ์คํจํฉ๋๋ค. ํ ์คํธ๋ฅผ ๊ฐ์ ํ๋ฉฐ, ํ ์คํธ๊ฐ ๊ฒ์ฆ๋๋ฉด ์์ฑ๋๋ ๋ฌธ์ ๋ํ ์ ๋ขฐํ ์ ์๋ค๋ ์ฅ์ ์ด ์กด์ฌํฉ๋๋ค.
2. ์ค์
log
Some problems were found with the configuration of task ':asciidoctor' (type 'AsciidoctorTask').
- In plugin 'org.asciidoctor.convert' type 'org.asciidoctor.gradle.AsciidoctorTask' method 'asGemPath()' should not be annotated with: @Optional, @InputDirectory.
์ค์ ์ ์ํด ๊ณต์ ๋ฌธ์์ ๊ธฐ์ ๋ ๋ฐฉ๋ฒ์ ์ฐธ๊ณ ํ์ผ๋ ์ ์๋ฌ์ ๊ณ์ ์ง๋ฉดํ์ต๋๋ค.
build.gradle
plugins {
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
Gradle 7 ๋ถํฐ๋ org.asciidoctor.convert
๊ฐ ์๋ asciidoctor.jvm.convert
๋ฅผ ์ฌ์ฉํ๋ค๊ณ ํฉ๋๋ค.
build.gradle
dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
REST Docs๋ฅผ RestAssured ํน์ WebTestClient๋ก ์์ฑํ๊ณ ์ถ๋ค๋ฉด spring-restdocs-webtestclient
ํน์ spring-restdocs-restassured
๋ก ๊ต์ฒดํ๋ฉด ๋ฉ๋๋ค.
build.gradle
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
useJUnitPlatform()
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
Gradle ๋ฌธ๋ฒ์ ์ต์ํ์ง ์์ง๋ง ์์๋๋ฉด ์ข์ ๊ฐ๋ ๋ค์ ์ ๋ฆฌํ์ต๋๋ค.
dependsOn
: ํน์ Task๊ฐ ์์กดํ๋ Task๋ฅผ ๋ช ์ํฉ๋๋ค.- ์ฆ,
asciidoctor
Task๋ ์์กดํ๋test
Task ์ดํ์ ์ํ๋ฉ๋๋ค.
- ์ฆ,
finalizedBy
: ํน์ Task์ ํํ Task๋ฅผ ๋ช ์ํฉ๋๋ค.asciidoctor
Task์finalizedBy copy
๋ผ๊ณ ๋ช ์๋์ด ์์ผ๋ฉด,asciidoctor
Task ์ดํ copy Task๊ฐ ์คํ๋ฉ๋๋ค.
build.gradle
ext {
snippetsDir = file('build/generated-snippets')
}
์์ฑ๋ ์ค๋ํซ์ ์ ์ฅ ์์น๋ฅผ ๋ช ์ํฉ๋๋ค.
build.gradle
test {
outputs.dir snippetsDir
useJUnitPlatform()
}
ํ ์คํธ Task์ ์์ํ ๋๋ ํ ๋ฆฌ๋ฅผ ์ค๋ํซ ์ ์ฅ ์์น๋ก ์ค์ ํฉ๋๋ค.
build.gradle
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
์ค๋ํซ ์ ์ฅ ์์น๋ฅผ ์ธํ์ผ๋ก ์ง์ ํฉ๋๋ค. asciidoctor
Task๋ ํ
์คํธ Task ์ํ ์ดํ์ ์ํ๋ฉ๋๋ค. ๋ฐ๋ผ์ ๋ฌธ์๊ฐ ์์ฑ๋๊ธฐ ์ ์ ํ
์คํธ๊ฐ ์ํ๋๋๋ก ๋ณด์ฅํฉ๋๋ค. ๋ฌผ๋ก ๊ผญ ์์กด์ฑ์ ์์ ๊ฐ์ด ์ค์ ํ์ง ์๊ณ Jenkins Pipeline์์ Task๋ฅผ ์ ๋นํ ์์์ ๋ง์ถฐ ์ํํด๋ ๋ฉ๋๋ค.
3. ํ ์คํธ ์ฝ๋ ์์ฑ
PostControllerTest.java
// when, then
ResultActions resultActions = mockMvc.perform(get("/api/posts/{id}", "1")
.accept(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(content().string(expected));
verify(postService, times(1)).readById(1L);
// restDocs
resultActions.andDo(document("post-read-one",
preprocessRequest(prettyPrint()),
preprocessRequest(prettyPrint()),
responseFields(fieldWithPath("id").description("๊ฒ์๋ฌผ id"),
fieldWithPath("title").description("์ ๋ชฉ"),
fieldWithPath("content").description("๋ด์ฉ"),
fieldWithPath("author").description("๊ธ์ด์ด"),
fieldWithPath("viewCounts").description("์กฐํ์"),
fieldWithPath("createdDate").description("์์ฑ์ผ"),
fieldWithPath("modifiedDate").description("์์ ์ผ"),
fieldWithPath("imageUrls").description("์ฌ์ง ๋งํฌ")))
);
MockMvcRestDocumentation
public static RestDocumentationResultHandler document(String identifier,
OperationRequestPreprocessor requestPreprocessor, OperationResponsePreprocessor responsePreprocessor,
Snippet... snippets) {
//...
}
andDo()
๋ฉ์๋์ document()
๋ฅผ ์ถ๊ฐํ์ฌ ์ค๋ํซ์ ์์ฑํฉ๋๋ค. ๊ฐ์ฅ ์ฒซ ๋ฒ์งธ ์ธ์๋ ์ค๋ํซ์ Identifier์
๋๋ค.
preprocessRequest(prettyPrint())
๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ์์ฒญ๊ณผ ์๋ต์ ๋ด์ฉ์ ํ์ํํจ์ผ๋ก์จ ๊ฐ๋
์ฑ์ด ๋์์ง๋๋ค. document()
๋ฉ์๋๋ ์ค๋ฒ๋ก๋ฉ๋์ด ์์ด์ prettyPrint()
๋ฅผ ์์ฒญ, ์๋ต, ์์ฒญ ๋ฐ ์๋ต ๋ชจ๋ ๋ฑ ์ด 3๊ฐ์ง์ ์ ์ฉํ ์ ์์ต๋๋ค.
Snippet์ ๊ฒฝ์ฐ ์์ฒญ ํค๋, ์๋ต ํค๋, ์๋ต ๋ณธ๋ฌธ ๋ฐ์ดํฐ ํ๋ ๋ฑ ๋ค์ํ๊ฒ ์์ฑํ ์ ์๋๋ฐ์. ์ฌ์ฉ ์์ ๋ฐ ๊ณต์ ๋ฌธ์๋ฅผ ๋ณด๋ฉด์ ํ์ตํ๋ฉด ์ดํด๊ฐ ๋น ๋ฅผ ๊ฒ์ ๋๋ค.
PostControllerTest.java
// restDocs
resultActions.andDo(document("post-read-multiple",
getDocumentRequest(),
getDocumentResponse(),
responseFields(fieldWithPath("simplePostResponses[].id").description("๊ฒ์๋ฌผ id"),
fieldWithPath("simplePostResponses[].title").description("์ ๋ชฉ"),
fieldWithPath("simplePostResponses[].content").description("๋ด์ฉ"),
fieldWithPath("simplePostResponses[].author").description("๊ธ์ด์ด"),
fieldWithPath("simplePostResponses[].viewCounts").description("์กฐํ์"),
fieldWithPath("simplePostResponses[].createdDate").description("์์ฑ์ผ"),
fieldWithPath("simplePostResponses[].modifiedDate").description("์์ ์ผ"),
fieldWithPath("startPage").description("์์ ํ์ด์ง"),
fieldWithPath("endPage").description("๋ ํ์ด์ง"),
fieldWithPath("prev").description("์ด์ ํ์ด์ง ์ฌ๋ถ"),
fieldWithPath("next").description("๋ค์ ํ์ด์ง ์ฌ๋ถ")))
);
List๊ฐ ํฌํจ๋ ์๋ต์ []
๋ฅผ ํตํด ์ฒ๋ฆฌํฉ๋๋ค. ๋ง์ฝ ์๋ต์ด ๋จ์ผ ๋ฆฌ์คํธ๋ง์ ํฌํจํ๊ณ ์๋ค๋ฉด ์์ ์ ๋ค๋ฅด๊ฒ [].id
, [].title
๊ณผ ๊ฐ์ด ์์ฑํ ์ ์์ต๋๋ค.
PostControllerTest.java
resultActions.andDo(document("post-write-login",
getDocumentRequest(),
getDocumentResponse(),
requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("Bearer token")),
requestPartBody("images"),
responseHeaders(headerWithName(HttpHeaders.LOCATION).description("๊ฒ์๋ฌผ ์ฃผ์")))
);
ํค๋ ๋ํ ๊ฒ์ฆ์ด ๊ฐ๋ฅํฉ๋๋ค.
Gradle ์ต์ ์์ Test๋ฅผ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ค๋ํซ์ด ์์ฑ๋ฉ๋๋ค. ํ ์คํธ ์ฝ๋๋ฅผ ํตํด ์์ฑ๋ ์ค๋ํซ๊ณผ ์ฌ์ฉ์๊ฐ ๋ฏธ๋ฆฌ ์์ฑํด๋ ํ ํ๋ฆฟ ๋ฌธ์๋ฅผ ๊ฒฐํฉํด ์ต์ข API HTML ๋ฌธ์๋ฅผ ์์ฑํฉ๋๋ค.
์ฌ์ฉ์๊ฐ ๋ฏธ๋ฆฌ ์์ฑํด๋ ํ์ผ์ Gradle ๊ธฐ์ค src/docs/asciidoc
์ ์์น์ํต๋๋ค. ์ด ๋ ํ์ผ ํ์ฅ์๋ .adoc
์
๋๋ค.
api.adoc
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]
== User
=== ํ์ํํด (๋น๋ก๊ทธ์ธ)
==== Request
include::{snippets}/withdraw-not-login/http-request.adoc[]
==== Response
include::{snippets}/withdraw-not-login/http-response.adoc[]
=== ํ์ํํด (๋ก๊ทธ์ธ)
=== ๊ฒ์๋ฌผ ๋จ๊ฑด ์กฐํ
==== Request
include::{snippets}/post-read-one/http-request.adoc[]
==== Response
include::{snippets}/post-read-one/http-response.adoc[]
==== Fields
include::{snippets}/post-read-one/response-fields.adoc[]
์ ์ ๋ธ๋ก๊ทธ๋ฅผ ๊พธ๋ฏธ๋ฏ์ด API ๋ฌธ์ ๋ํ ์กฐ๊ธ๋ง ๋ ๋ ธ๋ ฅ์ ๋ค์ด๋ฉด ๋งค์ฐ ์๋ฆ๋ต๊ฒ ๊ตฌ์ฑํ ์ ์์ต๋๋ค. ๋ํ ๊ฐ๊ฐ์ ์ค๋ํซ identifier ํด๋ ํ์์๋ ์ฒจ๋ถํ ์ ์๋ ๋ค์ํ adoc๋ค์ด ์กด์ฌํ๋๋ฐ, ์ํฉ์ ๋ง๊ฒ ์ ์ ํํ์๋ฉด ๋ฉ๋๋ค.
build.gradle
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'BOOT-INF/classes/static/docs'
}
}
ํด๋น ์ค์ ์ ์ถ๊ฐํด์ฃผ๊ณ , ์ด๋ฒ์๋ Gradle ์ต์ ์์ bootJar๋ฅผ ์คํํด๋ด ์๋ค.
bootJar๋ฅผ ์คํํ๋ฉด src/docs/asciidoc
์ ์์น์ํจ ์ฌ์ฉ์๊ฐ ์ ์ํ adoc ํ์ผ๊ณผ ํ
์คํธ ์ฝ๋์์ ์์ฑ๋ ์ค๋ํซ๋ค์ด ์กฐํฉ๋ ์ต์ข
API ๋ฌธ์๊ฐ build/docds/asciidoc
๋๋ ํ ๋ฆฌ์ ์์ฑ๋ฉ๋๋ค.
build.gradle
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'BOOT-INF/classes/static/docs'
}
finalizedBy 'copyDocument'
}
task copyDocument(type: Copy) {
dependsOn bootJar
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
bootJar Task๋ฅผ ํตํด api.html
์ด ์์ฑ๋๋ฉด, ํํ Task๋ก copyDocument๊ฐ ์คํ๋๋๋ก ํฉ์๋ค. copyDocument๊ฐ ์ํ๋๋ฉด classpath(resources)๋ก API ๋ฌธ์๊ฐ ๋ณต์ฌ๋ฉ๋๋ค.
bootJar๋ก ์คํํ๋ฉด ์ปดํ์ผ์ ๊ฑฐ์น๊ณ ํ ์คํธ Task๊ฐ ์์๋ฉ๋๋ค. ํ ์คํธ Task๊ฐ ์ข ๋ฃ๋๋ฉด ์์กด ๊ด๊ณ์ ๋ฐ๋ผ asciidoctor, bootJar, copyDocument Task๊ฐ ๊ฐ๊ฐ ์คํ๋ฉ๋๋ค.
์ดํ๋ฆฌ์ผ์ด์ ์ ์คํํ๋ฉด ์ ์ ์๋๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
3. ๊ธฐํ
build.gradle
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
๋ง์ฝ ๋ฌธ์์ ์์ ์ด ๋ฐ์ํ์์๋ ๋ฐ์๋์ง ์๋๋ค๋ฉด ๋ค์ ํ์คํฌ๋ฅผ ์ถ๊ฐํ์ฌ ๋ณต์ฌ ์ด์ ์ ๊ธฐ์กด์ ์๋ ํด๋์คํจ์ค์ API ๋ฌธ์๋ฅผ ์ ๊ฑฐํ๋๋ก ํฉ์๋ค.
Reference
- Spring REST Docs
- [Spring] Spring rest docs ์ ์ฉ๊ธฐ(gradle 7.0.2)
- Configuring asciidoctor when using Spring Restdoc