Spring REST Docs ๋ฅผ ์ฌ์ฉํด ๋ฌธ์ ์๋ํํ๋ ๋ฐฉ๋ฒ + ์ํ ํ๋ก์ ํธ ์์ฑ (Spring +JPA)
* ํน์๋ ๋ฐ๋ก ์ฝ๋๋ถํฐ ๋ณด๊ณ ์ถ์ ์ฌ๋๋ค์
-> " Spring REST Docs ๋ฅผ ์ด์ฉํ Sample Project " ๊ฒ์ ๋ ์ญ๊ณ
0๏ธโฃ Spring REST Docs ๋ ?
: Spring MVC Test ๋๋ WebTestClient์ผ๋ก ์์ฑ๋ ์ค๋ํซ๊ณผ ์ง์ ์์ฑํ ๋ฌธ์๋ฅผ ๊ฒฐํฉํ์ฌ Restful ์๋น์ค๋ฅผ ๋ฌธ์ํํ๋ ๊ฒ์ด๋ค.
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction
Spring REST Docs
Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.
docs.spring.io
-> Spring REST Docs ๊ด๋ จ ๊ณต์ ๋ฌธ์์ด๋ค. ์ค๋ช ์ด ์ ๋์ด ์์ด์ ์ฌ์ค ์ฌ๊ธฐ์ ๋์จ๋๋ก ๋ฐ๋ผ์ ํ๋ฉด ๋ฌธ์๋ฅผ ๋ง๋ค ์ ์๋ค.
Spring REST Docs๋ ๊ธฐ๋ณธ์ ์ผ๋ก Asciidoctor๋ฅผ ์ฌ์ฉํ๋ค. Asciidoctor๋ ์ผ๋ฐ ํ ์คํธ๋ฅผ ์ฒ๋ฆฌํ๊ณ ํ์์ ๋ง๊ฒ ์คํ์ผ๊ณผ ๋ ์ด์์์ ๊ฐ์ถ HTML ์ ์์ฑํด์ค๋ค. (์ฌ๊ธฐ์๋ ์ค๋ํซ์ ๋ชจ์ ๊ฒฐํฉํ ๊ฒ์ HTML ๋ก ๋ณํํด์ค๋ค.)
Spring REST Docs ๋ Spring MVC์ ํ ์คํธ ํ๋ ์ ์ํฌ, Spring WebTestClient ๋๋ REST Assured 5๋ก ์์ฑ๋ ํ ์คํธ์์ ์์ฑ๋ ์ค๋ํซ์ ์ฌ์ฉํ๋ค.
์ด ํ ์คํธ ๊ธฐ๋ฐ ์ ๊ทผ ๋ฐฉ์์ ์๋น์ค ๋ฌธ์์ ์ ํ์ฑ์ ๋ณด์ฅํ๋๋ฐ ๋์์ด ๋๋ค. -> ์ ๋ขฐ์ฑ !! ์๋ํ๋ฉด ํ ์คํธ์ ์คํจํ๋ ์ผ์ด์ค๊ฐ ์๋ค๋ฉด ๋ฌธ์๋ฅผ ๋ง๋ค ์ ์๋ค.
Swagger ๋์ Spring REST Docs ๋ฅผ ์ฌ์ฉํ ์ด์ ?
์ฌ์ค ๋ ์ค ๋ญ๊ฐ ๋ ๋ซ๋ค ! ๋ผ๊ณ ๋ง์ ํ ์ ์๋ค. ๋ ๋ค ์ฅ๋จ์ ์ด ์๊ธฐ ๋๋ฌธ์ด๋ค.
- Swagger๋ ์ ์ฉ์ด ์ฝ๊ณ , ๋ฌธ์์์ ๋ฐ๋ก ํธ์ถํด ํ ์คํธํด๋ณผ ์ ์๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ํ๋ก๋์ ์ฝ๋์ ์นจํฌ์ ์ด๋ฉฐ ํ ์คํธ์๋ ๋ฌด๊ดํด์ ์ ๋ขฐ์ฑ์ ๋จ์ด์ง๋ค๋ ๋จ์ ์ด ์๋ค.
- Spring REST Docs ๋ Swagger์ ๋ค๋ฅด๊ฒ ํ ์คํธ๋ฅผ ํต๊ณผํด์ผ ๋ฌธ์๊ฐ ๋ง๋ค์ด์ง๊ธฐ ๋๋ฌธ์ ์ ๋ขฐ๋๊ฐ ๋๊ณ , ํ๋ก๋์ ์ฝ๋์ ๋ถ๋ฆฌํด์ ํ ์คํธ ์ฝ๋์์ ๋ฌธ์๋ฅผ ์์ฑํ๊ธฐ ๋๋ฌธ์ ์นจํฌ์ ์ด์ง ์๋ค. ํ์ง๋ง Swagger์ ๋นํ๋ฉด ํจ์ฌ ์ฝ๋์์ด ๋ง๊ณ , ์ค์ ์ด ์ด๋ ต๋ค๋ ๋จ์ ์ด ์๋ค.
=> ์ฐ์ ์ฒ์์ ๋๋ Swagger๋ฅผ ํตํด ์์ฑํ๋ฉด ์ฌ์ค ์ฝ๋๋ ๊ฐํธํ๋๊น ์ข๋ค๊ณ ์๊ฐํ๋ค.
๊ทผ๋ฐ ๋ฌธ์ ์ปค์คํ ์ ์ํด์๋ ์ ์ ๋ฉ์ธ ์ฝ๋๋ค์ ๋ถํ์ํ ์ด๋ ธํ ์ด์ ๋ค์ด ๋ค์ด๊ฐ๋๊น ์ฝ๋๊ฐ ์ง์ ๋ถํด ์ง๋๊ฒ ์ซ์๋ค. ๊ทธ๋ฆฌ๊ณ api ๋ฌธ์๋ฅผ ๋ง๋ค๊ณ ์ถ์๊ฒ์ด๋ฏ๋ก Spring REST Docs๊ฐ ๋ ๋ซ๋ค๋ ์๊ฐ์ผ๋ก ๋ฐ๋์๋ค.
๊ตณ์ด ์ง์ ํธ์ถํด ํ ์คํธ ํ๋ ๊ฒ์ ์ธํ ๋ฆฌ์ ์ด์์ ์ฌ์ฉ ๊ฐ๋ฅํ httpd ๋ฅผ ํ์ฉํ๋ฉด ๋๋ค๊ณ ์๊ฐํ๋ค. (ํด๋ผ์ด์ธํธ ์ ์ฅ์ ์๋์ง๋ง)
๊ทธ๋ฆฌ๊ณ ์ฐ๋ฆฌ ํ์ฌ ๊ธฐ์ค? ์ด๊ธฐ๋ ํ์ง๋ง api ๋ฌธ์๋ฅผ v0.1 ์ด๋ฐ์์ผ๋ก ๋ง๋๋๋ฐ ์์ ํ๊ณ ํ์ผ์ ๊ณต์ ํด๋ ์ด๋ค ์ฌ๋์ ์์ง๋ 0.1 ๋ฒ์ ๋ณด๊ณ ์๊ณ , ์ด๋ค ์ฌ๋์ 0.2 ๋ฒ์ ์ ๋ณด๊ณ ์๊ณ ,,? ์ต์ ๋ฌธ์๋ฅผ ๋ณด์ง ์๋ ๊ฒฝ์ฐ๊ฐ ์๋ ๊ฒ ๊ฐ์๋ค.
๋ฐ๋น ์ ๋ฉ์ผ ํ์ธ์ ๋ฐ๋ก๋ฐ๋ก ๋ชปํ๋ฉด ๊ทธ๋ด ์ ์๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋์ ์ด๋ด๊ฑฐ๋ฉด ์ฝ๋ ์์ ํ ํญ์ ํ ์คํธ ํ๊ณ ์ฑ๊ณตํ๋ฉด api ๋ฌธ์๋ ๋ง๋ค์ด์ง๋๊น ์ด๋ ๊ฒ ํ์ธํ๋๊ฒ ๋ ๋ซ๋ค๊ณ ์๊ฐํ๋ค.
์ฌ์ค ์ค์ ์ด์ผ ์ฒ์์๋ง ์ด๋ ต๋ค๊ณ ์๊ฐํ๋ค. ๊ทธ๋ฆฌ๊ณ Asciidoc ๋ฌธ๋ฒ์ด ์ด๋ ต๋ค๊ณ ๋๊ปด์ง ์ ์๋ค. ๊ทผ๋ฐ ์ด๊ฑด ์ฌ์ค ์ฒ์ ํด๋ด์ ๊ทธ๋ฐ๊ฑฐ ์๋๊น? ๋ผ๋ ์๊ฐ... ์ต์ํด์ง๋ฉด ์ด๊ฒ ๋ ๋ซ๋ค๊ณ ์๊ฐํ๋ค.
์ฒ์์๋ ๋ฌผ๋ก ์๊ฐ๋ ์ค๋ ๊ฑธ๋ฆฌ๊ณ , ์ฝ๋ ์๋ ๋ง์์ ๊ท์ฐฎ์์ง ์ ์์ง๋ง, word๋ก ํ๊ทธ๋ฆฌ๊ณ , ๊ฑฐ๊ธฐ์ ์ค๋ช ์ฐ๊ณ ํ๋ ๊ฒ๋ณด๋ค ์ด๊ฒ ํจ์ฌ ์ข๋ค๊ณ ์๊ฐํ๋ค. (์ด๊ฑด ๋ด๊ฐ ์๋ ์์ ์ ๋ณ๋ก ์์ข์ํด์ ์ผ์๋...)
์ํผ ์ด๋ฐ ์ ๋ฐ ์ด์ ๋ก ๋๋ Spring REST Docs๋ฅผ ์ ํํ๋ค.
1๏ธโฃ Spring REST Docs ์์ํ๊ธฐ + ์ค์
์ํ ํ๋ก์ ํธ๋ฅผ ํตํด์ ์ค์ ํ๋ ๋ฐฉ๋ฒ, ๋ฌธ์ ๋ง๋๋ ๋ฐฉ๋ฒ๋ค์ ๊ฐ๋จํ๊ฒ ์์๋ณผ ์์ ์ด๋ค.
์ต์ ์๊ตฌ์ฌํญ
- ์๋ฐ 17
- ์คํ๋ง ํ๋ ์์ํฌ 6
์ํ ํ๋ก์ ํธ ํ๊ฒฝ
- ์๋ฐ 17
- ์คํ๋ง ๋ถํธ 3.2.2
- JPA + H2
- Junit5 + MockMVC
- AsciiDoc
build.gradle
plugins { (1)
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
asciidoctorExt (2)
}
dependencies {
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3)
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4)
}
ext { (5)
snippetsDir = file('build/generated-snippets')
}
test { (6)
outputs.dir snippetsDir
}
asciidoctor { (7)
inputs.dir snippetsDir (8)
configurations 'asciidoctorExt' (9)
dependsOn test (10)
}
-> ๊ณต์ ๋ฌธ์์ ๋์์๋ build.gradle ์ค์ ๋ฐฉ์์ด๋ค.
(1) AsciiDoc ํ์ผ์ ์ปจ๋ฒํ ํ๊ณ Build ํด๋์ ๋ณต์ฌํ๊ธฐ ์ํ ํ๋ฌ๊ทธ์ธ์ด๋ค.
(2) Asciidoctor ๋ฅผ ํ์ฅํ๋ ์ข ์์ฑ์ ๋ํ ๊ตฌ์ฑ์ ์ ์ธํ๋ค.
(3) asciidoctorExt ๊ตฌ์ฑ์ spring-restdocs-asciidoctor์ ๋ํ ์ข ์์ฑ์ ์ถ๊ฐํ๋ค. ๊ทธ๋ฌ๋ฉด .adoc ํ์ผ์์ ์ฌ์ฉํ snippets ์์ฑ์ด build/generated-snippets๋ฅผ ๊ฐ๋ฆฌํค๋๋ก ์๋์ผ๋ก ๊ตฌ์ฑ๋๋ค. ๋ํ ์์ ๋ธ๋ก ๋งคํฌ๋ก๋ฅผ ์ฌ์ฉํ ์๋ ์๋ค.
(4) mockmvc๋ฅผ restdocs์ ์ฌ์ฉํ ์ ์๊ฒ ํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ → ๋ง์ฝ Rest Assured ๋ฅผ ์ฌ์ฉํ๊ณ ์ ํ๋ค๋ฉด ๋ค๋ฅธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉํด์ผํจ
(5) ์์ฑ๋ ์ฝ๋ ์กฐ๊ฐ์ ์ถ๋ ฅ ์์น๋ฅผ ์ ์ํ๋ snippetsDir ์์ฑ
(6) Gradle ์ด ํ ์คํธ ์์ ์ ์คํํ๋ฉด ์ถ๋ ฅ์ด snippetsDir์ ๊ธฐ๋ก๋๋ค.
(7) asciidoctor task๋ฅผ ๊ตฌ์ฑํ๋ค.
(8) ์์ ์ ์คํํ๋ฉด snippetsDir์์ ์ฝ๋๋ค.
(9) ํ์ฅ์ ์ํด asciidoctorExt ์ ๊ตฌ์ฑํ๋ค.
(10) ํ ์คํธ๊ฐ ๋จผ์ ์คํ๋๊ณ ๋ฌธ์๊ฐ ์์ฑ๋๋๋ก ํ๋ค.
(11) gradle build ์ asciidoctor ๋จผ์ ์ํ ํ, bootJar ๊ฐ ์ํ๋๋ค.
(12) gradle build ์ build/generated-snippets/html5์ html ํ์ผ์ด ์๊ธฐ๊ณ , gradle build ์ static/docs ํด๋์ ๋ณต์ฌ๋๋ค.
์์ : test -> asciidoctor -> bootJar
ํ ์คํธ ์ค์
: Spring REST Dcos๋ Junit5 ๋ฐ Junit4๋ฅผ ์ง์ํ๊ณ , Junit5 ์ฌ์ฉ์ ๊ถ์ฅํ๋ค.
@ExtendWith(RestDocumentationExtension.class) // (1)
public class JUnit5ExampleTests {
private MockMvc mockMvc;
@BeforeEach // (2)
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
}
(1) RestDocumentationExtension ํ ์คํธ ํด๋์ค ์ ์ฉ
(2) MockMvc ๊ตฌ์ฑํ๊ธฐ ์ํด @BeforeEach ๋ฉ์๋๋ฅผ ์ ๊ณตํด์ผ ํ๋ค.
-> ์ฌ์ค ์ด๋ ๊ฒ ๋๊ฐ๋ง ์ค์ ํด์ฃผ๋ฉด ๊ธฐ๋ณธ ์ค์ ์ ๋ค ํ ๊ฒ์ด๊ณ , ์ด์ ๋ฌธ์๋ฅผ ๋ง๋ค๊ธฐ ์ํ ์ฝ๋๋ฅผ ์์ฑํด์ฃผ๋ฉด ๋๋ค.
2๏ธโฃ Restful ์๋น์ค ํธ์ถ , ์์ฒญ ๋ฐ ์๋ต ๋ฌธ์ํ
Restful ์๋น์ค ํธ์ถ
this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) // (1)
.andExpect(status().isOk()) // (2)
.andDo(document("index")); // (3)
(1) get ์์ฒญ์ผ๋ก ์๋น์ค์ ๋ฃจํธ๋ฅผ ํธ์ถํ๋ค.
(2) ์๋น์ค๊ฐ ์์ํ๋ ์๋ต์ ์์ฑํ๋์ง ํ์ธํ๋ค.
(3) ์๋น์ค์ ๋ํ ํธ์ถ์ ๋ฌธ์ํํ์ฌ “index” ๋ผ๋ ๋๋ ํฐ๋ฆฌ์ ์ฝ๋ ์กฐ๊ฐ์ ์์ฑํ๋ค. ์ค๋ํซ์ RestDocumentationResultHandler์ ์ํด์ ์์ฑ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ธฐ๋ณธ์ ์ผ๋ก 6๊ฐ์ ์ค๋ํซ์ด ์๊ธด๋ค.
- <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
์ค๋ํซ ์ฌ์ฉ
์์ฑ๋ ์ค๋ํซ์ ์ฌ์ฉํ๊ธฐ ์ ์ ์์ค ํ์ผ์ ์์ฑ ํด์ผ ํ๋ค. ๊ทธ๋๋ค์ src/docs/asciidoc/(ํ์ผ๋ช ).doc ์ผ๋ก ์์คํ์ผ์ ์์ฑํ๋ฉด ๋๋ค.
์์ฑ ํ,
include::{snippets}/index/curl-request.adoc[]
→ ์ด๋ฐ์์ผ๋ก include๋ฅผ ์ฌ์ฉํ์ฌ ์๋์ผ๋ก ์์ฑ๋ Asciidoc ํ์ผ์ ์์ฑ๋ ์กฐ๊ฐ์ ํฌํจํ ์ ์๋ค.
API ๋ฌธ์ํ
- ์์ฒญ ๋ฐ ์๋ต ํ๋
{
"contact": {
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
}
this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("index", responseFields( // (1)
fieldWithPath("contact.email").description("The user's email address"), // (2)
fieldWithPath("contact.name").description("The user's name"))));
(1) org.springframework.restdocs.payload.PayloadDocumentation์ ์ ์ ๋ฉ์๋๋ก ์๋ต ํ์ด๋ก๋์ ํ๋๋ฅผ ์ค๋ช ํ๋ ์กฐ๊ฐ์ ์์ฑํ๋ ์ฝ๋ → ์์ฒญ์ ๋ฌธ์ํ ํ๋ ค๋ฉด requestFields๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
(2) path๊ฐ ์๋ ํ๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋๋ค.
→ ์์ ์ฝ๋์ ๊ฒฐ๊ณผ๋ก๋ ์๋ต ํ๋๋ฅผ ์ค๋ช ํ๋ ํ ์ด๋ธ์ด ํฌํจ๋ ์ค๋ํซ์ผ๋ก, ์๋ต์ด๋ฏ๋ก response-fields.adoc์ผ๋ก ์์ฑ๋๋ค.
(์ด๋ ๋ฌธ์ํ๋์ง ์์ ํ๋๋ค์ด ์กด์ฌํ ๊ฒฝ์ฐ ํ ์คํธ ์คํจํ๋๋ฐ ๋ชจ๋ ํ๋๋ฅผ ๋ฌธ์๋ก ์ ๊ณตํ๊ณ ์ถ์ง ์์ ๋๋ ํ์ด๋ก๋์ ์ ์ฒด ํ์ ์น์ ์ ๋ฌธ์ํํ ์๋ ์๋ค.)
- ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์
this.mockMvc.perform(get("/users?page=2&per_page=100"))
.andExpect(status().isOk())
.andDo(document("users", queryParameters( // (1)
parameterWithName("page").description("The page to retrieve"), // (2)
parameterWithName("per_page").description("Entries per page")
)));
(1) ์์ฒญ์ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์๋ฅผ ์ค๋ช ํ๋ ์กฐ๊ฐ์ ์์ฑํ๋๋ก Spring REST Docs ๋ฅผ ๊ตฌ์ฑํ๋ค. org.springframework.restdocs.request.RequestDocumentation. ์ queryParameters์ ์ ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.
(2) ๋งค๊ฐ๋ณ์๋ค์ ๋ฌธ์ํ ํ ๋๋ parameterWithName๋ฅผ ์ฌ์ฉํ๋ค.
- ๊ฒฝ๋ก ๋งค๊ฐ๋ณ์
this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275))
.andExpect(status().isOk())
.andDo(document("locations", pathParameters( // (1)
parameterWithName("latitude").description("The location's latitude"), // (2)
parameterWithName("longitude").description("The location's longitude")
)));
(1) ์์ฒญ์ ๊ฒฝ๋ก ๋งค๊ฐ๋ณ์๋ฅผ ์ค๋ช ํ๋ ์กฐ๊ฐ์ ์์ฑํ๋๋ก Spring REST Docs ๋ฅผ ๊ตฌ์ฑํ๋ค. ์ด๋ org.springframework.restdocs.request.RequestDocumentation.์ pathParameters ์ ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.
(2) ๋งค๊ฐ๋ณ์๋ฅผ ๋ฌธ์ํํ ๋๋org.springframework.restdocs.request.RequestDocumentation.์ parameterWithName ์์ ์ ์ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ค.
→ ์์ ์ฝ๋์ ๊ฒฐ๊ณผ๋ก๋ path parameters ํ๋๋ฅผ ์ค๋ช ํ๋ ํ ์ด๋ธ์ด ํฌํจ๋ ์ค๋ํซ์ผ๋ก, ์๋ต์ด๋ฏ๋ก path-parameters.adoc์ผ๋ก ์์ฑ๋๋ค.
(์ด๋ ๋ฌธ์ํ๋์ง ์์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ค์ด ์กด์ฌํ๋ฉด ํ ์คํธ ์คํจํ๋๋ฐ, ๋ง์ฝ ์ฟผ๋ฆฌ ๋งค๊ฐ๋ณ์๋ฅผ ๋ฌธ์ํํ์ง ์์ผ๋ ค๋ฉด ํด๋น ๋งค๊ฐ๋ณ์๋ฅผ ๋ฌด์๋จ์ผ๋ก ํ์ํ ์ ์๋ค. )
* ๋ํ์ ์ธ ๊ธฐ๋ฅ๋ค๋ง ์ ์ํ ๊ฒ์ผ๋ก, ๋ ์์ธํ ๊ธฐ๋ฅ์
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction
→ Spring REST Docs ๊ณต์ ๋ฌธ์ ์ฐธ์กฐ
3๏ธโฃ Spring REST Docs ๋ฅผ ์ด์ฉํ Sample Project
build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.2'
id 'io.spring.dependency-management' version '1.1.4'
id "org.asciidoctor.jvm.convert" version "3.3.2" // Asciidoctor plugin ์ ์ ์ฉํด์ผ ํจ (sciiDoc ๋ฌธ๋ฒ์ผ๋ก ์์ฑํ๋ฉด Asciidoctor๋ฅผ ์ด์ฉํด์ html๋ก ๋ณํํด์ค๋ค.)
}
group = 'com.sample'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
asciidoctorExt // Asciidoctor ๋ฅผ ํ์ฅํ๋ ์ข
์์ฑ์ ๋ํ ๊ตฌ์ฑ์ ์ ์ธ
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
// RestDocs
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
ext {
// snippetsDir ์์ฑ๋ ์กฐ๊ฐ(snipppets)์ ์ถ๋ ฅ ์์น๋ฅผ ์ ์ํ๋ ์์ฑ์ ๊ตฌ์ฑ
snippetsDir = file('build/generated-snippets')
}
test {
// test ์์
์ ์คํํ๋ฉด ์ถ๋ ฅ์ด snippetsDir์ ๊ธฐ๋ก๋๋ค๋ ์ ์ Gradle์ด ์ธ์ํ๋๋ก ํ๋ค.
outputs.dir snippetsDir
}
asciidoctor { // asciidoctor ์์
์ ๊ตฌ์ฑ
inputs.dir snippetsDir // 7. ์์
์ ์คํํ๋ฉด snippetDir์์ ์
๋ ฅ์ ์ฝ๊ฒ๋๋ค๊ณ Gradle์ ์ธ์
configurations 'asciidoctorExt'
dependsOn test // test ์ํํ ๋ค์์ asciidoctor๋ฅผ ์ํํ๋ค.
}
bootJar {
dependsOn asciidoctor // test -> asciidoctor -> bootJar
copy {
from asciidoctor.outputDir
into 'src/main/resources/static/docs'
}
}
+spring.profiles.active = real (์ค์๋ฒ) ์ผ ๊ฒฝ์ฐ์ Spring REST Docs ๋ฌธ์ ์์ฑ ๋ชปํ๋๋ก ๋ง๋ ๋ฐฉ๋ฒ
bootJar {
dependsOn asciidoctor // test -> asciidoctor -> bootJar
String activeProfile = System.properties['spring.profiles.active']
if(activeProfile != "real") {
copy {
from asciidoctor.outputDir
into 'src/main/resources/static/docs'
}
}
}
-> ์ค์ ์ด์ ํ๊ฒฝ์์ api ๋ฌธ์๊ฐ ์์ฑ๋๋ฉด ์๋๊ธฐ ๋๋ฌธ์ bootJar ์คํํ ๋ ์ด๋ ๊ฒ ๋ง์๋๋ฉด ๋ฌธ์ ๋ณต์ฌ๋ฅผ ์ํ๋ค!
*๋ฌธ์์์ฑ์ ์ํ ์ํ ํ๋ก์ ํธ ์ฝ๋ (ํ๋ก๋์ ์ฝ๋)
: ๊ทธ๋ฅ ํ ์คํธ ์ฝ๋๋ง ๋ณด์ฌ์ค๊น.. ํ๋ค๊ฐ..๋ญ ์ฝ๋๊ฐ ๋ง์ ๊ฒ๋ ์๋๋.. ๊ทธ๋ฅ ์ฝ๋ ๋ค ๊ณต๊ฐํฉ๋๋ค! (์ฐธ๊ณ ํ์ธ์ )
Post
package com.sample.restDocs.vo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@NoArgsConstructor
public class Post
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
@Builder
public Post(Long id, String title, String content)
{
this.id = id;
this.title = title;
this.content = content;
}
public void change(String title, String content)
{
this.title = title;
this.content = content;
}
}
-> ๊ฐ๋จํ๊ฒ ์ ๋ชฉ, ๋ด์ฉ ์์ฑ๋ง ํ ์ ์๊ฒ ํ๋ค.
PostController
package com.sample.restDocs.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.controller.response.ApiResponse;
import com.sample.restDocs.service.response.PostResponse;
import com.sample.restDocs.service.PostService;
import lombok.RequiredArgsConstructor;
@RequestMapping("/api/v1")
@RequiredArgsConstructor
@RestController
public class PostController
{
private final PostService postService;
/**
* ๊ธ ์์ฑ
* @param request
* @return
*/
@PostMapping("/post")
public ApiResponse<PostResponse> writePost(@RequestBody PostCreateRequest request)
{
return ApiResponse.ok(postService.writePost(request));
}
/**
* ๊ธ ๋จ๊ฑด ์กฐํ
* @param postId
* @return
*/
@GetMapping("/post/{postId}")
public ApiResponse<PostResponse> getPost(@PathVariable(value = "postId") Long postId)
{
return ApiResponse.ok(postService.getPost(postId));
}
/**
* ๊ธ ์์
* @param postId
* @param request
* @return
*/
@PostMapping("/post/{postId}")
public ApiResponse<PostResponse> updatePost(@PathVariable(value = "postId")Long postId, @RequestBody PostEditRequest request)
{
return ApiResponse.ok(postService.editPost(postId, request));
}
}
-> ์ํ ํ๋ก์ ํธ๋ก ๋ง๋ค ๊ฑด ๊ฐ๋จํ๊ฒ ๊ธ ์์ฑ ๋ฐ ์์ , ์กฐํํ๋ api ์ด๋ค.
( ์ด์ ๋๋ ๋ฌธ์ ๋ง๋๋ ค๊ณ ๊ธ ๋ณด๋ฌ์ค์ ๋ถ๋ค์ด๋ผ๋ฉด ๋ค ์๊ฑฐ๋ผ.. ์๊ฐํ๊ธฐ ๋๋ฌธ์ api ์ฝ๋์ ๋ํ ์ค๋ช ์ ์๋ฝํ๋๋ก ํ๊ฒ ์ต๋๋ค.. ํน์ ์ง๋ฌธ์ด ์๋ค๋ฉด ๋๊ธ ๋จ๊ฒจ์ฃผ์ธ์ ๐)
PostCreateRequest
package com.sample.restDocs.controller.request;
import com.sample.restDocs.vo.Post;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostCreateRequest
{
private String title;
private String content;
@Builder
public PostCreateRequest(String title, String content)
{
this.title = title;
this.content = content;
}
public Post toEntity()
{
return Post.builder()
.title(title)
.content(content)
.build();
}
}
PostEditRequest
package com.sample.restDocs.controller.request;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class PostEditRequest
{
private String title;
private String content;
@Builder
public PostEditRequest(String title, String content)
{
this.title = title;
this.content = content;
}
}
-> ์ฌ๊ธฐ์ PostCreateRequest์ PostEditRequest ๋ฅผ ์ ๋ถ๋ฆฌํ๋์ง ์ดํด ์๋ ์๋ ์๋ค.
์๋ํ๋ฉด ์ฝ๋๊ฐ ๊ฑฐ์ ๋๊ฐ๊ธฐ ๋๋ฌธ !!
์ฌ์ค ์ํ ํ๋ก์ ํธ๋ผ ๊ฐ์ ๋ณด์ด๋ ๊ฒ์ด๋ค. ์ด์ ํ๊ฒฝ์์๋ ๊ธ ์์ฑ, ์์ ๋ชจ๋ ์๊ตฌํ๋ ๋ฐ์ดํฐ๊ฐ ๋ค๋ฅผ ์ ์๋๋ฐ ์ด๊ฑธ ํ๋๋ก ํต์ผํด์ ์ฌ์ฉํ๋ ๊ฑด ์๋๋ผ๊ณ ์๊ฐํ๋ค. ๊ทธ๋ฅ ํ์ํ ๋ฐ์ดํฐ๋ง ๋ฑ ์๋ dto๋ฅผ ๋ง๋ค๊ณ ์ถ์๋ค.
(๊ตณ์ด ์ํํ๋ก์ ํธ์์ ์ด๋ ๊ฒ ์๋๋ ๋ ๋๋ค!! ์ ํ ์ฌํญ์ด๋ค.)
(์ฐ๋ฆฌ xx์์ ํ๋์ vo์ ๋ค ๋๋ ค๋ฃ๋๋ฐ... ๊ทธ๋ฌ๋ฉด ํ๋์ vo์ ๋ถํ์ํ ๋ฐ์ดํฐ๊ฐ ๋๋ฌด ๋ง์ด ๋ค์ด๊ฐ๊ณ ,,, ํ vo์ ๊ฐ์ด 20๊ฐ๊ฐ ๋๊ธฐ๋ ํ๋ค... ์ ์ด๋ ๊ฒ ํ๋์ง ๋์ ํ ๋ชจ๋ฅด๊ฒ ์ง๋ง.. dto ๋ง๋ค์๋ค๊ฐ ์ญ์ ํ๋ผ๊ณ ํ ์๋ฆฌ๋ค์ด์... ์ํ ํ๋ก์ ํธ์์๋งํผ์ด๋ผ๋ ๋ด ๋ง๋๋ก ํ๋ค!!!! ใ ใ ์ฌ์กฑ์.. ๋์ด๊ฐ์ธ์ )
ApiResponse
package com.sample.restDocs.controller.response;
import org.springframework.http.HttpStatus;
import lombok.Getter;
@Getter
public class ApiResponse<T>
{
private int code;
private HttpStatus status;
private String message;
private T data;
public ApiResponse(HttpStatus status, String message, T data)
{
this.code = status.value();
this.status = status;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> of(HttpStatus status, String message, T data)
{
return new ApiResponse<>(status, message, data);
}
public static <T> ApiResponse<T> of(HttpStatus status, T data)
{
return new ApiResponse<>(status, status.name(), data);
}
public static <T> ApiResponse<T> ok(T data)
{
return of(HttpStatus.OK, data);
}
}
-> ์ปจํธ๋กค๋ฌ์์ ๊ทธ๋ฅ vo ๊ฐ์ฒด๋ฅผ ๋๊ฒจ๋ ๋๊ฒ ์ง๋ง.. ๋ณดํต์ ํด๋ผ์ด์ธํธ์ ํ์ํด์ ์ด๋ค ํ์์ผ๋ก ์ค ์ง ์ ํ๋ค.
๊ทธ๋ฌ๋ฏ๋ก code, message, status, data ์ ๋๋ก ํด์ ApiResponse๋ฅผ ๋ง๋ค์๋ค.
PostService
package com.sample.restDocs.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.service.response.PostResponse;
import com.sample.restDocs.vo.Post;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class PostService
{
private final PostRepository postRepository;
public PostResponse getPost(Long postId)
{
Post post = postRepository.findById(postId).orElseThrow(() -> new RuntimeException("ํด๋น Post๋ ์กด์ฌํ์ง ์์ต๋๋ค."));
return PostResponse.builder().title(post.getTitle())
.content(post.getContent()).build();
}
public PostResponse writePost(PostCreateRequest request)
{
Post requestPost = request.toEntity();
Post save = postRepository.save(requestPost);
return PostResponse.builder().title(save.getTitle())
.content(save.getContent()).build();
}
@Transactional
public PostResponse editPost(Long postId, PostEditRequest request)
{
Post post = postRepository.findById(postId).orElseThrow(() -> new RuntimeException("ํด๋น Post๋ ์กด์ฌํ์ง ์์ต๋๋ค."));
post.change(request.getTitle(), request.getContent());
return PostResponse.builder().title(post.getTitle())
.content(post.getContent()).build();
}
}
PostResponse
package com.sample.restDocs.service.response;
import lombok.Builder;
import lombok.Getter;
@Getter
public class PostResponse
{
private String title;
private String content;
@Builder
public PostResponse(String title, String content)
{
this.title = title;
this.content = content;
}
}
-> ์ฌ๊ธฐ์ ๋ ์์ํ ? ์ฌ๋๋ค์ด ์์ ๊ฒ ๊ฐ๋ค.
์ ์๋น์ค ๊น์ง ์ด๋ ๊ฒ ๋ง๋๋!! ํ ์ ์๋ค. ํ์ง๋ง ์ปจํธ๋กค๋ฌ์์๋ ๊ฐ ์ฒดํฌ๋ ํด์ผํ๊ณ , ์ด๋ฐ ๋ก์ง์ด ๋ค์ด๊ฐ ๊ฑด๋ฐ, ์๋น์ค์์๋ ์ด ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ ์ฌ์ฉํ๊ธฐ๋ณด๋ค๋ ์ปจํธ๋กค๋ฌ์ ์๋น์ค๋ ๋ถ๋ฆฌํด์ ์ฌ์ฉํ๋๊ฒ ๋ ๋ซ๋ค~ ๋ผ๋ ๊ฐ์๋ฅผ ๋ค์๋ค.
์ปจํธ๋กค๋ฌ ์ ์ฑ ์ ์๋น์ค๊น์ง ๊ฐ์ ธ์ค์ง ๋ง๋ผ~ ๋ญ ์ด๋ฐ ๊ฒ ๊ฐ๋ค.
(์ด๊ฒ๋ ์ฌ์ค ์ํ ํ๋ก์ ํธ์์๊น์ง ํ ํ์๋ ์์ง๋ง ๋๋ ์ต๊ด ๋ค์ด๊ณ ์ถ์๋ค.)
PostRepository
package com.sample.restDocs.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.sample.restDocs.vo.Post;
@Repository
public interface PostRepository extends JpaRepository<Post, Long>
{
}
*๋ฌธ์์์ฑ ์ํ ํ๋ก์ ํธ ์ฝ๋ (ํ ์คํธ ์ฝ๋)
PostControllerTest
package com.sample.restDocs.controller;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.service.PostService;
import com.sample.restDocs.vo.Post;
@AutoConfigureMockMvc // @SpringBootTest ๋ฅผ ์ฌ์ฉํ๋ ํ
์คํธ์์ MockMvc๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ์ ์ฌ์ฉ
@SpringBootTest
class PostControllerTest
{
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private PostService postService;
@Autowired
private PostRepository postRepository;
@BeforeEach
void clean() {
postRepository.deleteAll(); // ํ
์คํธ ์ํ ํ ๋ฐ์ดํฐ ํด๋ ์ง
}
@DisplayName("๊ธ ์์ฑ ํ
์คํธ")
@Test
void write() throws Exception
{
// given
PostCreateRequest request = PostCreateRequest.builder().title("ํ
์คํธ ์ ๋ชฉ")
.content("ํ
์คํธ ๋ด์ฉ").build();
// when
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/post")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ
์คํธ ์ ๋ชฉ"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ
์คํธ ๋ด์ฉ"));
}
@DisplayName("๊ธ 1๊ฐ๋ฅผ ์ ์ฅ ํ ํด๋น ๊ธ ์กฐํ ํ
์คํธ")
@Test
void get() throws Exception
{
// given
Post post = Post.builder().title("ํ
์คํธ ์ ๋ชฉ").content("ํ
์คํธ ๋ด์ฉ").build();
Post response = postRepository.save(post);
// when
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/post/{postId}", response.getId()))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ
์คํธ ์ ๋ชฉ"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ
์คํธ ๋ด์ฉ"));
}
@DisplayName("๊ธ 1๊ฐ๋ฅผ ์์ ํ ํด๋น ๊ธ ์กฐํ ํ
์คํธ")
@Test
void update() throws Exception
{
// given
Post post = Post.builder().title("ํ
์คํธ ์ ๋ชฉ").content("ํ
์คํธ ๋ด์ฉ").build();
Post response = postRepository.save(post);
PostEditRequest editRequest = PostEditRequest.builder().title("ํ
์คํธ ์ ๋ชฉ ์์ !").content("ํ
์คํธ ๋ด์ฉ ์์ !").build();
// when
ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/post/{postId}", response.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(editRequest)))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ
์คํธ ์ ๋ชฉ ์์ !"))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ
์คํธ ๋ด์ฉ ์์ !"));
}
}
-> ์ด๊ฑด ๊ทธ๋ฅ PostController๋ฅผ ํ ์คํธ ํ๋ ์ฝ๋์ด๋ค. (์ฐธ๊ณ ..ใ )
์ฌ์ค ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ ๋ฒ์ ์ด์ ๋ง ๋ฐฐ์์ ๊ณต๋ถํ๊ณ ํ๋ ๋จ๊ณ๋ผ.. ํ ์คํธ ํ๋ ์ฝ๋๊ฐ ์๋ฒฝํ๋ค๊ณ ํ ์๋ ์์ง๋ง ์ผ๋จ ์ ๋ ์ํ ํ๋ก์ ํธ๋ก ๋ง๋ api ํ ์คํธ๋ฅผ ์ด๋ ๊ฒ ์์ฑํ์ต๋๋ค.
(๊ทธ๋ฆฌ๊ณ Service ๋ ๋ฐ๋ก ํ ์คํธํ๋๊ฒ ๋ง๋๋ฐ.. ์๊ฐ๊ด๊ณ์ ์ ๋ ์๋ตํ์ง๋ง..๋ง๋ค์ด๋ณด์๊ธธ... Service๋จ ํ ์คํธ๋ ํด์ผ ํฉ๋๋ค!!)
RestDocsSupport
package com.sample.restDocs.docs;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
/**
* Spring REST Docs ํ๊ฒฝ ํด๋์ค
*/
@AutoConfigureRestDocs( // Spring REST Docs ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด MockMvc ๋น ์ปค์คํ
uriScheme = "https", uriHost = "sample.restDocs.com"
)
@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class RestDocsSupport
{
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@BeforeEach
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation)
{
// @AutoConfigureMockMvc ๋ฅผ ํตํ ์๋์ฃผ์
์ด ์๋๋ผ ํ
์คํธ ์ ์ ๋ฏธ๋ฆฌ mockMvc ์ธํ
ํ๋ ์์
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
}
-> Rest Docs ๋ฌธ์๋ฅผ ๋ง๋ค๊ธฐ ์ํ ํ๊ฒฝ ์ค์ ? ์ด๋ผ๊ณ ํ ์ ์๋ค.
์ฐ์ @ExtendWith(RestDocumentationExtension.class)๋ฅผ ์ถ๊ฐํด์ค์ผ ํ๋ค.
๊ทธ๋ฆฌ๊ณ @AutoConfigureRestDocs๋ Spring REST Docs ๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด MockMvc ๋น ์ปค์คํ ํ๋ ๊ฑฐ๋ผ๊ณ ํ๋ค..!
-> @BeforeEach๋ฅผ ํตํด ํ ์คํธ ์ํ ์ ์ MockMvcRestDocumentationConfigurer. documentationConfiguration()์ ์ ์ ๋ฉ์๋ ์์ ์ด ํด๋์ค์ ์ธ์คํด์ค๋ฅผ ์ป๋๋ค.
PostControllerDocsTest
package com.sample.restDocs.docs;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.restdocs.request.RequestDocumentation;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
import com.epages.restdocs.apispec.ResourceDocumentation;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.epages.restdocs.apispec.Schema;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.vo.Post;
public class PostControllerDocsTest extends RestDocsSupport
{
@Autowired
private PostRepository postRepository;
protected List<FieldDescriptor> defaultResponseFieldDescriptors = new java.util.ArrayList<>(
List.of(
PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("์๋ต ์ฝ๋"),
PayloadDocumentation.fieldWithPath("status").type(JsonFieldType.STRING).description("์๋ต ์ํ"),
PayloadDocumentation.fieldWithPath("message").type(JsonFieldType.STRING).description("์๋ต ๋ฉ์์ง"),
PayloadDocumentation.fieldWithPath("data").type(JsonFieldType.OBJECT).description("์๋ต ๋ฐ์ดํฐ")
));
@DisplayName("๊ธ ํ๋๋ฅผ ์์ฑํ๋ฉด ์ ์์ ์ผ๋ก ์ ์ฅ๋๋ค.")
@Test
void write() throws Exception
{
// given
PostCreateRequest request = PostCreateRequest.builder().title("ํ
์คํธ ์ ๋ชฉ")
.content("ํ
์คํธ ๋ด์ฉ").build();
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ ๋ชฉ"));
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋ด์ฉ"));
// when
ResultActions result = mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/post")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("post-write", // REST Docs
RestDocsUtils.getDocumentRequest(),
RestDocsUtils.getDocumentResponse(),
PayloadDocumentation.requestFields( // ์์ฒญ ๋ฐ์ดํฐ ์คํ
PayloadDocumentation.fieldWithPath("title").description("post ์ ๋ชฉ").optional(),
PayloadDocumentation.fieldWithPath("content").description("post ๋ด์ฉ")
),
PayloadDocumentation.responseFields( // ์๋ต ๋ฐ์ดํฐ ์คํ
defaultResponseFieldDescriptors
)
));
;
}
@DisplayName("๊ธ 1๊ฐ๋ฅผ ์ ์ฅํ๊ณ ์กฐํํ๋ ํ
์คํธ")
@Test
void get() throws Exception
{
// given
Post post = Post.builder().title("ํ
์คํธ ์ ๋ชฉ").content("ํ
์คํธ ๋ด์ฉ").build();
Post response = postRepository.save(post);
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ ๋ชฉ"));
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋ด์ฉ"));
// when
ResultActions result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/post/{postId}", response.getId()))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("post-get", // REST docs
RestDocsUtils.getDocumentResponse(),
RequestDocumentation.pathParameters(
RequestDocumentation.parameterWithName("postId").description("post Id")
),
PayloadDocumentation.responseFields(
defaultResponseFieldDescriptors
)
));
}
@DisplayName("๊ธ 1๊ฐ๋ฅผ ์์ ํ ํด๋น ๊ธ ์กฐํ ํ
์คํธ")
@Test
void update() throws Exception
{
// given
Post post = Post.builder().title("ํ
์คํธ ์ ๋ชฉ").content("ํ
์คํธ ๋ด์ฉ").build();
Post response = postRepository.save(post);
PostEditRequest editRequest = PostEditRequest.builder().title("ํ
์คํธ ์ ๋ชฉ ์์ !").content("ํ
์คํธ ๋ด์ฉ ์์ !").build();
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ ๋ชฉ"));
defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋ด์ฉ"));
// when
ResultActions result = mockMvc.perform(
RestDocumentationRequestBuilders.post("/api/v1/post/{postId}", response.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(editRequest)))
.andDo(MockMvcResultHandlers.print());
// then
result.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(MockMvcRestDocumentation.document("post-update", // REST Docs
RestDocsUtils.getDocumentRequest(),
RestDocsUtils.getDocumentResponse(),
RequestDocumentation.pathParameters(
RequestDocumentation.parameterWithName("postId").description("post Id")
),
PayloadDocumentation.requestFields(
PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING).description("post ์ ๋ชฉ").optional(),
PayloadDocumentation.fieldWithPath("content").type(JsonFieldType.STRING).description("post ๋ด์ฉ")
),
PayloadDocumentation.responseFields(
defaultResponseFieldDescriptors
)
));
}
}
-> ์ด๋ ๊ฒ ํ ์คํธ ์ฝ๋ ์ ๋ฌธ์ ์์ฑ ์ฝ๋๋ฅผ ๋ง๋ค์ด์ฃผ๋ฉด ๋๋ค.
๊ทธ๋ฆฌ๊ณ bootJar ์คํํ๋ฉด
์ค๋ํซ์ด ์์ฑ๋๋ค.
-> ๊ทธ๋ฆฌ๊ณ src/main/resources/static/docs๋ก index.html ๋ก ๋ณํ๋์ด copy ๋ ๊ฒ๋ ํ์ธํ ์ ์๋ค.
์ค๋ํซ ์ฌ์ฉ ์ฝ๋
index.adoc
= RestDocs Test API
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
== ๊ธ ์์ฑ
=== ์์ฒญ
include::{snippets}/post-write/http-request.adoc[]
=== ์๋ต
include::{snippets}/post-write/http-response.adoc[]
include::{snippets}/post-write/response-fields.adoc[]
== ๊ธ ๋จ๊ฑด ์กฐํ
=== ์์ฒญ
include::{snippets}/post-get/http-request.adoc[]
include::{snippets}/post-get/path-parameters.adoc[]
=== ์๋ต
include::{snippets}/post-get/http-response.adoc[]
include::{snippets}/post-get/response-fields.adoc[]
-> ์์ฑ๋ ์ค๋ํซ์ ์ฌ์ฉํ๊ธฐ ์ ์ ์์ค ํ์ผ์ ์์ฑํด์ผ ํ๋ค.
src/docs/asciidoc์ index.adoc ์ ๋ง๋ค๊ณ ์ด๋ฐ์์ผ๋ก ์ค๋ํซ๋ค์ ์กฐํฉํด์ ๋ฌธ์๋ฅผ ๋ง๋๋ ๊ฒ์ด๋ค.
adoc ๋ฌธ์๋ ์ปค์คํ ๊ฐ๋ฅํ๊ณ , adoc ๋ฌธ๋ฒ์ ๊ณต๋ถํด์ ์ ์ฉํ๋ฉด ๋๋ค.
https://docs.asciidoctor.org/ → asciidoctor ๊ณต์ ๋ฌธ์
๊ฒฐ๊ณผ ํ๋ฉด
์ง๊ธ์ ์ํ ํ๋ก์ ํธ์ด๋ฏ๋ก http://localhost:8080/docs/index.html ์์ ํ์ผ api ๋ฌธ์๋ฅผ ํ์ธํ ์ ์๋ค.
๐ ๊ฐ๋จํ๊ฒ Spring REST Docs ๋ฅผ ์ด์ฉํด์ api ๋ฌธ์๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์์๋ดค๋ค.
๊ณต์ ๋ฌธ์๋ง ๋ณผ ๋ ๊น์ง๋ ??? ํ๋ ์ดํด ์๋๋ ๋ถ๋ถ๋ค์ด ์์๋๋ฐ ์ญ์ ์ง์ ๋ง๋ค์ด๋ณด๊ณ , ์ค๋ฅ ๋๋ฉด ์ฐพ์๋ ๋ณด๊ณ ํ๋ฉด์ ์ดํดํ๊ณ ๊ฒฐ๊ณผ ํ๋ฉด์ ๋ณด๋๊น ์ดํด๊ฐ ๋ ์ ๋ ๊ฒ ๊ฐ๋ค.
์ฌ์ค Spring REST Docs ๋ ๊ฐ๋ฐ์๊ฐ ์ผ๋ง๋ ๊ณต๋ค์ฌ์? ์์ฑํ๋๋์ ๋ฐ๋ผ์ ํ๋ฆฌํฐ๋ ๋ฌ๋ผ์ง ๊ฒ ๊ฐ๋ค.
ํ์ง๋ง ๋ญ.. ์ด์ ๋๋ง ํด๋ ๊น๋ํ๋ ๋ฌธ์๋ณด๋๋ฐ ์ด๋ ต์ง ์๋ค๊ณ ์๊ฐํ๋๋ฐ... ์ด๊ฑด ๋ด ์๊ฐ์ผ ๋ฟ.. ใ
์ด๊ฑด ์ง์ง ์ด๊ฐ๋จ์ผ๋ก ์ฌ์ฉ ๋ฐฉ๋ฒ์ ์ตํ๊ธฐ ์ํด ๋ง๋ ํ๋ก์ ํธ์ด๋ฏ๋ก ์ค์ ์์๋ ๋ ์ ๊ฒฝ์จ์ ํ ์คํธ๋ ์ํํด์ผ ํ๊ณ , ๋ฌธ์๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค.
Spring REST Docs๋ฅผ ์ค๋ฌด์์ ์ด์ฉํ๋ค๋ฉด ๋จผ์ api ๋ฌธ์๊ฐ ํ์ํ ํ ๋ฐ ๊ทธ๋ด๋ ์ด๋ป๊ฒ service.. ๋ฑ๋ฑ์ ์ธ์ ๋ง๋ค์ด์ ๋ฌธ์ ๊ณต์ ํ๊ฒ ๋๊ฐ..!
์ฐ์ tdd๋ก ํ๋ค๊ณ ์๊ฐํ๊ณ ControllerTest๋ง Mock์ฒ๋ฆฌ?ํด์ ํ ์คํธ ์ฝ๋ ์์ฑํ Spring rest doc์ผ๋ก ๋น ๋ฅด๊ฒ ๋ฌธ์๋ง ์์ฑํด์ ํด๋ผ์ด์ธํธ์ ๊ณต์ ํ๊ณ , ๊ทธ ๋ค์ ๋น์ง๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ ๋ฐฉ๋ฒ์ผ๋ก ์ด์ฉํ๋ฉด ๋ ๊ฒ ๊ฐ๋ค.