Spring REST Docs
์ค๋์ ์คํ๋ง REST Docs์ ๋ํด ์ ์ฉํ๊ณ , ๊ด๋ฆฌํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด๊ณ ์ ํฉ๋๋ค. ์คํ๋ง์ ์ด๋ ์ ๋ ์จ ๋ณด๊ณ , ํ ์คํธ ๋ฐ ๋ฌธ์ํ๋ฑ์ ํด ๋ณด์ ๋ถ๋ค์ด๋ผ๋ฉด ์ค์จ๊ฑฐ๋ ๋ ธ์ , ํฌ์คํธ๋งจ์ผ๋ก API ๋ฌธ์๋ฅผ ๋ง๋ ๊ฒฝํ์ด ์์ผ์ค ํ ๋ฐ์, ์คํ๋ง์ ์๋ REST Docs๋ ํ์ ์์ ๋ง์ด ์ฐ๋ ๋ฌธ์ํ ์คํฌ์ด๋ผ๊ณ ํ ์ ์์ต๋๋ค.
๊ธฐ์กด ๋ฌธ์ํ ๋๊ตฌ๋ค์ ์ค์จ๊ฑฐ์ ๊ฒฝ์ฐ ์ค์จ๊ฑฐ๋ฅผ ์ํ ์ฝ๋๊ฐ ํ๋ก๋์ ํ๊ฒฝ์ ๋ฌป๋๋ค๋ ์ , ๊ทธ๋ฆฌ๊ณ ์์ ์ ์ ๋ฐ์ดํธ ์ํ์ ๋ฌธ์์ ์ํ๊ฐ ์๋ก ๋ง์ง ์๋ ๋ฌธ์ ๋ฑ์ด ์์๋๋ฐ์, ์คํ๋ง REST Docs๋ ํ ์คํธ, ๋น๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์์ฑํ๊ธฐ์ ์ฑํฌ๊ฐ ์ ์ง๋ ์ ์๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
์๋ง ์์ํ๋ ๋จ๊ณ์์๋ถํฐ ์ด๋ป๊ฒ ์ ์ฉํด์ผ ํ ์ง ๊ฐ์ด ์ค์์ง ์๋ ๋ถ๋ค์ด ๋ง์ ๊ฒ ๊ฐ์, ์ ๊ฐ ํด๊ฒฐํ ๊ณผ์ ์ ๋จ๊ณ์ ์ผ๋ก ๊ณต์ ํ๊ณ ์ ํฉ๋๋ค!
1. build.gradle์ ํ์ํ ์ฝ๋ ์์ฑ
๊ธฐ์ ์ ์ ์ฉํ๊ธฐ ์ํด์๋ ์คํ๋ง REST Docs์ ๊ด๋ จ๋ ๋ด์ฉ๋ค์ build.gradle์ ์์ฑํด์ผ ํฉ๋๋ค. ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด๋ด ์๋ค.
1. Asciidoctor ํ๋ฌ๊ทธ์ธ ์ค์น
Asciidoctor ํ๋ฌ๊ทธ์ธ์ ์ค์นํฉ๋๋ค. Asciidoctor (์ค์ฌ์ adoc)์ ์คํ๋ง REST Docs์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ฌ์ฉํ๋ ํ์ผ ํ์์ ๋๋ค. ๋งํฌ๋ค์ด (Markdown)๊ณผ ๋น์ทํ๋ค๊ณ ์๊ฐํ์๋ฉด ๋ฉ๋๋ค.
Writing high-quality documentation is difficult. One way to ease that difficulty is to use tools that are well-suited to the job. To this end, Spring REST Docs uses Asciidoctor by default.
2. asciidoctorExt ์์ฑ
build.gradle ์ค์ (configuration)์ asciidoctorExt๋ฅผ ์์ฑํฉ๋๋ค. asciidoctor์ ํ์ฅ ํ์ผ (Extension)์ ๋ฑ๋กํด ๋ก๋๋ค.
3. spring-restdocs-asciidoctor ์์กด์ฑ ๋ฑ๋ก
asciidoctorExt ์์ spring-restdocs-asciidoctor ์์กด์ฑ์ ๋ฑ๋กํฉ๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ์ ํฌ๊ฐ ๋ค๋ฃฐ .adoc ํ์ผ์ ์๋ snippets๋ผ๋ ์ฉ์ด๊ฐ build/generated-snipptes๋ฅผ ๊ฐ๋ฆฌํค๋๋ก ํ ์ ์์ต๋๋ค. ๋ํ, operation ๋ธ๋ก ๋งคํฌ๋ก๋ฅผ ์งํํ ์ ์์ต๋๋ค. (์ด๊ฒ๋ค์ ์๋ ์์๋ฅผ ๋ณด๋ฉด ์ดํดํ์ค ์ ์์ต๋๋ค.)
4. testImplementation์ spring-restdocs-mockmvc ๋ฑ๋ก
REST Docs๋ mockMvc์ andDo ๋จ๊ณ์์ ์์ ์ ํฉ๋๋ค. ๋ง์ฝ mockMvc๋ฅผ ์ฌ์ฉํ์ง ์๊ณ WebTestClient๋ REST Assured๋ฅผ ์ฌ์ฉํ๋ค๋ฉด spring-restdocs-webtestclient, spring-restdocs-restassured๋ฅผ ์์ฑํ์๋ฉด ๋ฉ๋๋ค.
5. snippetsDir ๋ช ์
์์ 3๋ฒ์์ spring-restdocs-asciidoctor๋ฅผ ๋ฑ๋กํ๋ฉด ์ค๋ํซ (snippets) ์ฉ์ด๊ฐ ์๋์ผ๋ก build/generated-snipptes๋ฅผ ๊ฐ๋ฆฌํจ๋ค๊ณ ํ์์ต๋๋ค. ๊ทธ๋ฌ๋ ๋๋ก๋ ์ด ์์น์ ๊ดํด ๊ธฐ๋ณธ๊ฐ์ด ์ด๋ค ๊ฒ์ธ์ง๋ฅผ ๋ชจ๋ฅผ ์๋ ์๊ณ , ๊ฒฝ๋ก๋ฅผ ๋ฐ๊พธ๊ณ ์ถ์ ๋๋ ์์ ๊ฒ์ ๋๋ค. ๊ทธ๋ฌํ ๊ฒฝ์ฐ ์ด๋ ๊ฒ snippetsDir๋ฅผ ์์ฑํฉ๋๋ค.
6. ํ ์คํธ ๊ฒฐ๊ณผ๋ฌผ ์์น ์ ์
ํ ์คํธ์ ๋ํ ์ถ๋ ฅ์ด snipptesDir์ ์ ์ฅ๋๋๋ก ํฉ๋๋ค.
7 ~ 10. asciidoctor ๋ช ๋ น ์ต์ข ์ ์
build.gradle์์ asciidoctor๋ฅผ ์คํํ ๋, snipptesDir์ ์ ์ถ๋ ฅ ์์น๋ก ์ค์ ํ๊ณ asciidoctorExt๋ฅผ ๊ตฌ์ฑ ์ ๋ณด๋ก ์ฌ์ฉํ๋๋ก ํฉ๋๋ค.
2. ์ค์ ์ ์ฉ
์ค์ต ํ ์คํธ๋ ์ด์ ๊ฒ์๋ฌผ์์ ์์ฑํ ๊ฒ์๊ธ ์์ฑ/์กฐํ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๊ฒ ์ต๋๋ค.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
@WebMvcTest(BoardController.class)
public class BoardControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private BoardService boardService;
@Test
void ๊ฒ์๊ธ์_์ ์ฅํ๋ค() throws Exception {
// given
BoardWriteRequest request = new BoardWriteRequest("test title", "test content");
Board newBoard = ๊ฒ์๊ธ_id_์์();
when(boardService.write(request)).thenReturn(newBoard);
// when & then
mockMvc.perform(post("/boards")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(jsonPath("$.id").value(newBoard.getId()))
.andExpect(jsonPath("$.title").value(newBoard.getTitle()))
.andExpect(jsonPath("$.content").value(newBoard.getContent()))
.andDo(print())
.andDo(document("boards/save"));
}
@Test
void ๊ฒ์๊ธ์_์กฐํํ๋ค() throws Exception {
// given
Board writeBoard = ๊ฒ์๊ธ_id_์์();
when(boardService.findBoardById(any())).thenReturn(writeBoard);
// when & then
mockMvc.perform(get("/boards/" + writeBoard.getId()))
.andExpect(jsonPath("$.title").value(writeBoard.getTitle()))
.andExpect(jsonPath("$.content").value(writeBoard.getContent()))
.andDo(print())
.andDo(document("boards/find"));
}
}
ํฌ๊ฒ ๋ณด์ค ๋ถ๋ถ์ @AutoConfigureRestDocs์ document("...")์ ๋๋ค.
@AutoConfigureRestDocs
ํด๋น ์ด๋ ธํ ์ด์ ์ RestDocs์ ๋ํ ๊ธฐ๋ณธ ์ค์ ์ ์๋์ผ๋ก ์ ์ฉํด ์ค๋๋ค.
๊ณต์ ๋ฌธ์๋ฅผ ๋ณด๋ฉด, JUnit๊ณผ MockMvc๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ (ํด๋น ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ง ์์ ๊ฒฝ์ฐ) ์๋์ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํด์ผ ํ์ต๋๋ค.
// import ํํ์ ์๋ต
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@MockBean(JpaMetamodelMappingContext.class)
@WebMvcTest(BoardController.class)
public class BoardControllerTest {
// @Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private BoardService boardService;
@RegisterExtension
final RestDocumentationExtension restDocumentation = new RestDocumentationExtension("build/generated-snippets/boards");
@BeforeEach
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
...
}
RestDocumentationExtension์ RestDocumentationContext (REST API๊ฐ ์ํ๋ ์ปจํ ์คํธ๋ฅผ ์บก์ํํ ์ธํฐํ์ด์ค)๋ฅผ ์๋์ผ๋ก ๊ด๋ฆฌํ๋ JUnit์ ํ์ฅ (Extension)์ ๋๋ค. build/generated-snippets/boards๋ ๋ฌด์์ ์๋ฏธํ ๊น์? ๋ด๋ถ ์ฝ๋๋ฅผ ๋ณด๋ฉด ์ถ๋ ฅ ๊ฒฝ๋ก (outputDir)์ ์๋ฏธํจ์ ์ ์ ์์ต๋๋ค.
๊ทธ๋ฐ๋ฐ ์ฒ์์ build.gradle์์ ์ด๋ค ๊ฒ์ ์์ฑํ์๋์ง ๋ค์ ์๊ฐํด ๋ด ์๋ค. ์ค๋ํซ (snipptes)์ ๋ํด ์๋์ผ๋ก ๋ถ๋ฌ์ค๊ณ (spring-restdocs-asciidoctor), ์ฌ์ง์ด ๊ฒฝ๋ก๋ฅผ ๋ช ์ํ๊ธฐ๊น์ง ํ์ต๋๋ค. (snippetsDir = file('build/generated-snippets'))
๋ฐ๋ผ์ ์ด๋ฌํ ๋ด๋ถ ๊ณผ์ ์ ์๋์ผ๋ก ํด ์ฃผ๋ ๊ฒ ๋ฐ๋ก @AutoConfigureRestDocs์ ๋๋ค. ์ง์ ์์ฑํ ๋ ๋ณด๋ค ํ์ํ ์ฝ๋ ์์ด ์ค์ด๋๋ ํด๋น ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํ๋๋ก ํฉ์๋ค!
document("...")
์กฐ๊ธ ๋ง์ด ๋ฏ์ด๋ณด๊ฒ ๋ฉ๋๋ค! ์๋์๋ ์์ฑํ๋ฏ ๊ฒฐ๋ก ๋จผ์ ๋ง์๋๋ฆฌ๋ฉด ๋ถ์ธ ๋ฌธ์์ด์ ๊ฒฝ๋ก๋ก ํ์ฌ adoc ํ์ผ๋ค์ ์์ฑํ๋ค๋ ๊ฒ์ผ๋ก ์ดํดํ์๋ฉด ๋ฉ๋๋ค :)
๋ค์์ผ๋ก๋ document("...")๋ฅผ ๋ณด๊ฒ ์ต๋๋ค. ๊ฒฐ๋ก ๋จผ์ ๋ง์๋๋ฆฌ๋ฉด, {์ค๋ํซ ๊ฒฝ๋ก}/{document ์์ ์์ฑํ ๊ฒฝ๋ก}์ adoc ํ์ผ๋ค์ด ์์ฑ๋ฉ๋๋ค.
![](https://blog.kakaocdn.net/dn/czETKl/btsEy7zsp6V/8g2D0xfUnVvKMJOwfdO4sk/img.png)
์ด๊ฒ์ MockMvcRestDocumentation์์ ์์ฑ๋์ด ์๋ ๋ฉ์๋์ ๋๋ค.
public abstract class MockMvcRestDocumentation {
...
public static RestDocumentationResultHandler document(String identifier, Snippet... snippets) {
return new RestDocumentationResultHandler(new RestDocumentationGenerator(identifier, REQUEST_CONVERTER, RESPONSE_CONVERTER, snippets));
}
}
document("boards/save")๋ฅผ ํ ์, ์์ ์ ์๋ document ๋ฉ์๋๋ฅผ ์คํํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ RestDocumentationGenerator์ ์๋ handle ๋ฉ์๋๋ฅผ ์คํํฉ๋๋ค.
public final class RestDocumentationGenerator<REQ, RESP> {
...
public void handle(REQ request, RESP response, Map<String, Object> configuration) {
Map<String, Object> attributes = new HashMap(configuration);
OperationRequest operationRequest = this.preprocessRequest(this.requestConverter.convert(request), attributes);
OperationResponse operationResponse = this.preprocessResponse(this.responseConverter.convert(response), attributes);
Operation operation = new StandardOperation(this.identifier, operationRequest, operationResponse, attributes);
try {
Iterator var8 = this.getSnippets(attributes).iterator();
while(var8.hasNext()) {
Snippet snippet = (Snippet)var8.next();
snippet.document(operation);
}
} catch (IOException var10) {
throw new RestDocumentationGenerationException(var10);
}
}
...
}
์ด ๋ฉ์๋์์ request๋ ํ ์คํธ ํ๊ฒฝ์์ ๋ณด๋ธ HTTP request, response๋ ํ ์คํธ ํ๊ฒฝ์์ ๋ฐ์์ง HTTP response ์ ๋ณด๊ฐ ์์ต๋๋ค. configuration์๋ org.springframework.restdocs ํ์ ๊ตฌ์ฑ ์ ๋ณด (TemplateEngine, MockHttpServletRequest ๋ฑ)๊ฐ ์์ต๋๋ค.
![](https://blog.kakaocdn.net/dn/3VmXc/btsEydAe5RY/pI93nkcqwlbFM45U3oT98k/img.png)
attribute ์์ ์๋ RestDocumentationContext๋ ์์ build.gradle ๋ถ๋ถ์์ ๋งํ๋ฏ์ด REST API๊ฐ ์ํ๋ ์ปจํ ์คํธ๋ฅผ ์บก์ํํ ์ธํฐํ์ด์ค๋ผ๊ณ ํ์์ต๋๋ค. ๊ธฐ๋ณธ์ ์ธ ์ค๋ํซ ๊ฒฝ๋ก (build/generated-snippets)๋ฅผ ๊ฐ์ง๋๋ค.
![](https://blog.kakaocdn.net/dn/dUkEsf/btsEya4BsQD/sMwvOlVw8GWhZSxLLWhwAK/img.png)
operation์ Snippet์ด ์ํํ ๋ด์ฉ์ ์ ์ฒด์ ์ผ๋ก ๋ด๊ณ ์๋ ํด๋์ค์ ๋๋ค. ์์ "boards/save"๋ฅผ ์์ฑํ ๊ฒ์, ์ด operation์ ์ด๋ฆ (name)์ ๋ปํ๊ฒ ๋ฉ๋๋ค.
![](https://blog.kakaocdn.net/dn/1J8o6/btsEA4240yc/r18YSBSS242ahZpErBSEh1/img.png)
this.getSnippets๋ฅผ ๋ณด๋ฉด ์ adoc ํ์ผ๋ค์ด curl-request.adoc, http-request.adoc, http-response.adoc ๋ฑ ์ด 6๊ฐ๋ก ์์ฑ๋๋์ง๋ ์ ์ ์์ต๋๋ค.
![](https://blog.kakaocdn.net/dn/mLKC3/btsEAVL4LOQ/bdZojBjXbUo1ikvg1VH6DK/img.png)
๊ทธ ์ด์ ๋ org.springframework.restdocs.defaultSnippets์ 6๊ฐ์ snippetName์ด ์๊ธฐ ๋๋ฌธ์ ๋๋ค.
getSnippets ์์ ์ด ๋๋๋ฉด ๊ฐ๊ฐ ๊ฐ์ ธ์จ Snippet๋ค์ document ์์ ์ ํ๊ฒ ๋ฉ๋๋ค. Snippet์ ์๋์ฒ๋ผ 25๊ฐ์ง์ ๊ตฌํ ์ฝ๋๊ฐ ์์ต๋๋ค. ๊ทธ๋์ document๋ผ๋ ๋ก์ง์ ์ํํ ์ ์์ต๋๋ค.
![](https://blog.kakaocdn.net/dn/c65ux6/btsEBTNyiWE/MFLu7pJ1uua2ZktOeI1fL1/img.png)
CurlRequestSnippet์ TemplatedSnippet์ ์์๋ฐ์ผ๋ฉฐ, ์ด TemplatedSnippet์ Snippet์ ๊ตฌํํฉ๋๋ค. curl-request.adoc๊ณผ ๊ด๋ จ ์๋ ์ค๋ํซ์ ๋๋ค.
![](https://blog.kakaocdn.net/dn/cGNxA5/btsEyRjstaJ/5ViHMgVbSipKB2CFWj5vZK/img.png)
๋ฐ๋ก ์์์ operation์ ์ด๋ฆ (name)์ ์ ํฌ๊ฐ ์์ฑํ ๊ฒฝ๋ก๋ผ๊ณ ํ์์ต๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ค๋ํซ ์ด๋ฆ์ curl-request ๋ฑ์ ์๋ฏธํฉ๋๋ค.
RestDocumentationContext์๋ ๊ธฐ๋ณธ ์ค๋ํซ ๊ฒฝ๋ก (build/generated-snippets)๋ฅผ ๊ฐ์ง๊ณ ์์ต๋๋ค.
์ด๋ฒ์๋ writerResolver.resolve๋ฅผ ๋ณด๊ฒ ์ต๋๋ค!
public interface WriterResolver {
Writer resolve(String var1, String var2, RestDocumentationContext var3) throws IOException;
}
WriterResolver๋ ์ธํฐํ์ด์ค๋ก ๋์ด ์์ต๋๋ค. ๊ตฌํ์ฒด๋ StandardWriterResolver์ ๋๋ค.
![](https://blog.kakaocdn.net/dn/bks4kk/btsEAVL8WXz/VeB7xdKvB7Uq8lOnRz1hv1/img.png)
resolveFile์ ๋ณผ๊น์? ์ ๋ ๊ฒฝ๋ก๋ ์ ๋ ๊ฒฝ๋ก๊ฐ ์๋๋์ ๋ฐ๋ผ ๋ง๋ค์ด์ง๋ ํ์ผ์ด ๋ฌ๋ผ์ง์ ์ ์ ์์ต๋๋ค. ์ ํฌ๋ ์ ๋ ๊ฒฝ๋ก๋ก ์์ฑํ์ง ์์๊ธฐ ๋๋ฌธ์ (์ธ์๋ก ๋ฐ์ outputDirectory๋ boards/save์ ๋๋ค.) makeRelativeToConfiguredOutputDir๊ฐ ์คํ๋ฉ๋๋ค.
![](https://blog.kakaocdn.net/dn/bnoVgb/btsEyuhr2F9/EfxBjraCndwWipYQKNfcs0/img.png)
File์ ์์ฑ์๋ ์๋์ฒ๋ผ ๋์ด ์์ต๋๋ค.
![](https://blog.kakaocdn.net/dn/eaA0E7/btsExPl9Lmt/BMvAH7nHJ0UBGsKXypwfKK/img.png)
์ฒ์ ์ธ์๋ฅผ ๋ถ๋ชจ๋ก ๋ฐ๊ณ , ๋ ๋ฒ์งธ ์ธ์๋ฅผ ์์์ผ๋ก ํ๋๊ตฐ์! ๊ทธ๋์ ํ์ผ ๊ฒฝ๋ก๋ฅผ ๋ถ๋ชจ/์์์ผ๋ก ํจ์ ์ ์ ์์ต๋๋ค.
์์ฝํ์๋ฉด
- ํ ์คํธ ์ฝ๋์์ ๊ฒฝ๋ก์ ํจ๊ป document๋ฅผ ํธ์ถ
- MockMvcRestDocumentation์์ RestDocumentationGenerator ์์ฑ, ์์ฑํ ๊ฒฝ๋ก๋ฅผ identifier๋ก ์ทจํจ
- RestDocumentationGenerator์์ handle ๋ฉ์๋ ์คํ
- ์์ฑํ ๊ฒฝ๋ก, ์์ฒญ ์ ๋ณด, ์๋ต ์ ๋ณด, ๊ธฐํ ์ค์ ์ ๋ณด ์์ ์๋ ์์ฑ๋ค (RestDocumentationContext, MockHttpServletRequest ๋ฑ)์ ๊ฐ์ง Operation์ StandardOperation์ผ๋ก ์์ฑ
- ์์ฑ ์์ ์๋ defaultSnippets๋ค๋ก๋ถํฐ Snippet ๋ชฉ๋ก ์ทจํจ
- ๊ฐ Snippet๋ง๋ค์ document ๋ฉ์๋๋ฅผ ์คํ (Operation์ ์ธ์๋ก ๋๊น)
- TemplatedSnippet์ด Snippet์ ๊ตฌํํ๊ณ , ํ์ Snippet๋ค์ ์ ๋ถ TemplatedSnippet์ ์์๋ฐ์
- ๊ฒฐ๊ณผ์ ์ผ๋ก TemplatedSnippet์ document๊ฐ ์คํ๋จ, ์ธ์๋ก ๋ฐ์ Operation์ ์๋ RestDocumentationContext๋ฅผ ๋ฐ๊ณ , ๋ฐ์ operation์ ์ด๋ฆ (์์ฑํ identifier)๊ณผ RestDocumentationContext์ ์๋ outputDirectory(build/generated-snippets)๋ฅผ ๊ฒฐํฉํ์ฌ ์ต์ข ํ์ผ ๊ฒฝ๋ก๋ฅผ ์ง์ ํจ (build/generated-snippets/boards/save)
- ๊ฐ ์ค๋ํซ ํ์ผ์ด ์์ฑ๋จ
๋ผ๊ณ ํ ์ ์์ต๋๋ค!
๊ทธ๋์ ์ด๊ฒ๋ค์ ์ด๋ป๊ฒ ์ธ ์ ์๋?
์์ document ๋ก์ง์ด ๋ค์ ๋ณต์กํ๋๋ฐ ์ฌ๊ธฐ๊น์ง ์ฝ์ด์ฃผ์ ์ ๊ณ ์ํ์ จ์ต๋๋ค. ๊ทธ๋ผ ์ด์ ๋ ์ด๋ ๊ฒ ์์ฑ๋ adoc ํ์ผ๋ค์ ์ด์ฉํ ์ ์์ด์ผ๊ฒ ์ฃ . ์ด๋ ๊ฒ ํ๊ธฐ ์ํด์๋ ํ ํ๋ฆฟ (Template)์ ๋ง๋ค์ด์ผ ํฉ๋๋ค.
gradle์ ๊ฒฝ์ฐ src/docs/asciidoc์ adoc ํ์ผ๋ค์ ๋ง๋ค์ด์ผ ํ๋ค๊ณ ํฉ๋๋ค. ์ด adoc ํ์ผ๋ค์ ์ ํฌ๊ฐ html๋ก ๋ง๋ค adoc ํ์ผ, ์ฆ ๋ฌธ์ํํ adoc ํ์ผ์ ์๋ฏธํฉ๋๋ค.
์ ๋ ๋ค์๊ณผ ๊ฐ์ด src/docs/asciidocs์ board.adoc์ ์์ฑํ์ต๋๋ค.
snippets๋ผ๋ ์ฉ์ด๊ฐ build/generated-snippets๋ฅผ ๊ฐ๋ฆฌํจ๋ค๊ณ ํ์์ผ๋ฏ๋ก ์ ๋ ๊ฒ ์ถ์ฝํ ์ ์์ต๋๋ค.
๋ง์ฃผ์น ๋ฌธ์ ์ : html ํ์ผ์ด ์ ๋ณด์ธ๋ค!
์๋ง ์ด ๋จ๊ณ์์ ๊ณ ์ ํ์ ๋ถ๋ค์ด ์์ ๊ฒ ๊ฐ์ต๋๋ค. ๋ถ๋ช ์ ๋ ๊ฒ ์์ฑํ๋๋ฐ, html ํ์ผ์ด ๋ณด์ด์ง ์์ต๋๋ค.
์์ build.gradle์๋ ์๋ ์ฝ๋๋ฅผ ์์ฑํ์ง ์์์ง๋ง, ๊ณต์ ๋ฌธ์์์๋ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ผ๊ณ ํ์์ต๋๋ค.
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
์ถ๊ฐํด ๋ณด๋ฉด, html ํ์ผ์ด ๋ณด์ด๊ธด ํฉ๋๋ค. ๋จ, ๋น๋ ํจํค์ง์๋ง ๋ณด์ ๋๋ค.
๋ค๋ฅธ ๋ถ๊ป์ ์์ฑํ์ ๊ธ์ ์ฐธ๊ณ ํ๋, Gradle ๋ฒ์ ์ ์ํ ์ค๋ฅ๊ฐ ์์ธ์ธ ๊ฒ์ผ๋ก ์ถ์ ๋๋ค๊ณ ํฉ๋๋ค. (์ ๋ gradle 8.5 ์ฌ์ฉ ์ค์ ๋๋ค.) ๊ทธ๋์ ์์ build.gradle ์ฝ๋๋ฅผ ์๋์ฒ๋ผ ๋ฐ๊พธ๊ฒ ๋์์ต๋๋ค.
tasks.register('copyDocument', Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc/")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
build/docs/asciidoc์ ์๋ html ํ์ผ๋ค์ src/main/resources/static/docs์ ๋ณต์ฌํ๋ ๋ฐฉ์์ ๋๋ค.
์ค์ ๋ก html ํ์ผ์ ํ์ธํ๋ ค๋ฉด ์คํ๋ง ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ ๋ค localhost:8080/docs/board.html ๋ฑ์ผ๋ก ์ ๊ทผํ์๋ฉด ๋ฉ๋๋ค.
๊ฒฐ๋ก
์ฌ๊ธฐ๊น์ง REST Docs์ ๋ํ ๊ธฐ๋ณธ ํ๊ฒฝ์ค์ ๊ณผ document์ ๋ํ ์๋ฆฌ, html ํ์ผ ํ์ธ ๋ฑ์ ์์๋ดค์ต๋๋ค.
์ด์ ๋จ์ ๊ฒ์ ์ผ๋ง๋ ๋ ๋ฌธ์ํ๋ฅผ ๊น๋ํ๊ฒ ํ๊ณ , MockMvc์์ ๋ฌธ์ํ์ ๋ํ ๋ด์ฉ์ ์ถ๊ฐํ๋ ๋ฑ์ด ๋จ์๋๋ฐ, ์ด ๋ถ๋ถ์ ์์ง ์ด๋ณด๋ค ๋ณด๋ ๋ ์ตํ ๋ค์ ์ ๋ฆฌํ๋๋ก ํ๊ฒ ์ต๋๋ค.
ํน์ฌ๋ ์๋ชป๋ ๋ถ๋ถ์ด ์๋ค๋ฉด ๋๊ธ ๋ถํ๋๋ฆฝ๋๋ค!
Reference
- ์คํ๋ง ๊ณต์ ๋ฌธ์ - REST Docs
- ์คํ๋ง ๊ณต์ ๋ฌธ์ - RestDocumentationExtension class
- [Docs] Spring Rest Docs HTML ์ถ๋ ฅํ๋ ๋ฒ.
- [Spring] Rest Docs ๋์ ์ค ๋ง๋ฅ๋จ๋ฆฐ asciiDoctor์ ๋ํ ์๋ฌ
24.03.05 ์์ ๋ณธ: build.gradle ์ฝ๋
์ต์ข ์ ์ธ build.gradle ์ฝ๋๋ฅผ ์ ๋ฌํด๋๋ฆฝ๋๋ค.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'restdocs'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
asciidoctorExt
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
useJUnitPlatform() // ์ด๊ฑธ ์จ์ผ snippet๋ค์ด ๋ง๋ค์ด์ง๋ ๊ฒ ํ๊ฐ๋ฉ๋๋ค.
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn test
}
tasks.register('copyDocument', Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc/")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
- src/docs/asciidoc/์ด๋ฆ.adoc ํ์์ผ๋ก adoc ํ์ผ๋ค์ ๋ง๋ค์ด๋์ผ ํฉ๋๋ค.
- src/main/resources/static/docs ํจํค์ง๋ฅผ ๋ง๋ค์ด๋์ผ ํฉ๋๋ค.