์ค๋๋ง์ ๊ธฐ์ ํ์ ์์ฑํ๊ฒ ์ต๋๋ค. ์ฌ์ค JWT ๊ด๋ จํด์ ๊ธ์ ๊ณ์ ์์ฑํ๊ณ ์์๋๋ฐ, ํ๋ก์ ํธ๋ ์๊ณ ๊ฐ๊ฐ์ ํ ์ง ์ผ๋ง ๋์ง ์์ ๋ฐ์ ๊ฒ๋ ์์ด์ ํ ๊ธ์ ๊ฑฐ์ ํ ๋ฌ ๋ง์ ์ฌ๋ฆฌ๋ค์.. ๐ญ ํ๋ก์ ํธ๋ฅผ ํ๋ฉฐ ๋ฐ์๊ฒ ๋ ๊ฒ๋ ์์ง๋ง ๊ทธ ๋๋ถ์ REST Docs์ ๋ํด ๋ ์ ๋ค๋ฃฐ ์ ์๊ฒ ๋์ด ์ด ๊ณผ์ ์ ๊ณต์ ํฉ๋๋ค.
์ด์ ๊ธ์์๋ document ๋ฉ์๋๋ก ๊ฐ๋จํ docs ํจํค์ง ์๋์ html ๋ฌธ์๋ค์ด ๋ณด๊ด๋๋ ๊ฒ๊น์ง ๋ค๋ฃจ์๋๋ฐ์, ์ด๋ฒ์๋ ํ ์คํธ๋ฅผ ํตํด ์ค์ ๋ฌธ์๊ฐ ์์ฑ๋๋๋ก ํ๋ ๊ฒ์ ์์๋ณด๊ฒ ์ต๋๋ค.
๊ธฐ์กด ํ ์คํธ ์ฝ๋
๊ธฐ์กด ํ ์คํธ ์ฝ๋๋ ์๋์ ๊ฐ์ต๋๋ค.
// import ํํ์ ์๋ต
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
@WebMvcTest(BoardController.class)
public class BoardControllerWebMvcTest {
@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"));
}
}
์ด์ ๋ฐ๋ฅธ REST Docs ๊ฒฐ๊ณผ๋ ์๋์ฒ๋ผ ๋์ต๋๋ค. (์์ฒญ์ ๊ฒฝ์ฐ)
curl-request.adoc

curl-request๋ CURL ์์ฒญ ํ์์ ๋ณด์ฌ์ค๋๋ค.
http-request.adoc
http-request.adoc์ ์ค์ HTTP์์ ์ด๋ค ์์ผ๋ก ์์ฒญ์ด ํ๋ฌ๊ฐ๋์ง (HTTP ๋ฉ์๋, ๊ฒฝ๋ก ๋ฑ)๋ฅผ ๋ชจ๋ ๋ด๊ณ ์์ต๋๋ค.

request-body.adoc
request-body.adoc์ HTTP ์ค body ๋ถ๋ถ๋ง์ ๋ณด์ฌ์ค๋๋ค.

๊ธฐ์กด ๋ฌธ์์ ์์ฌ์ด ์
๊ธฐ์กด ๋ฌธ์๋ ์ ํํ title, content ๋ฑ์ด ์ด๋ค ๋ป์ธ์ง๋ฅผ ๋ช ์ํ์ง ์์ต๋๋ค. ๋ํ, ๋ง์ฝ ํค๋์ ์ธ์ฆ ์ ๋ณด (Authorization)๊ฐ ๋ค์ด๊ฐ๋ค๋ฉด ์ด๋ฌํ ๋ถ๊ฐ์ ์ธ ์ ๋ณด๋ ๋ด์ ์ ์์ต๋๋ค. (์ด๋ ์๋ต์์๋ ๋์ผํฉ๋๋ค.)
๋ฐ๋ผ์, restdocs ์์ ์๋ ๋ฉ์๋๋ค๋ก ์ค๋ช ์ ์ง์ ์์ฑํ ์ ์์ต๋๋ค.
Request, Response Fields
๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ์ฌ Request์ Response์ ํ๋๋ฅผ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.

์์ฒญ๊ณผ ์๋ต์ ํ๋๋ org.springframework.restdocs.payload.PayloadDocumentation์ requsetFields, responseFields๋ฅผ ํ์ฉํ์ฌ ๋ด์ฉ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
package org.springframework.restdocs.payload;
import ...
public abstract class PayloadDocumentation {
private PayloadDocumentation() {
}
...
public static RequestFieldsSnippet requestFields(FieldDescriptor... descriptors) {
return requestFields(Arrays.asList(descriptors));
}
public static RequestFieldsSnippet requestFields(List<FieldDescriptor> descriptors) {
return new RequestFieldsSnippet(descriptors);
}
...
public static ResponseFieldsSnippet responseFields(FieldDescriptor... descriptors) {
return responseFields(Arrays.asList(descriptors));
}
public static ResponseFieldsSnippet responseFields(List<FieldDescriptor> descriptors) {
return new ResponseFieldsSnippet(descriptors);
}
...
}
์์ฒญ๊ณผ ์๋ต์ ๊ตฌ์กฐ๋ฅผ ๋ณด๋, FieldDescriptor๊ฐ ๊ณตํต์ผ๋ก ์ฐ์ด๋ ๊ฒ ๋ณด์ ๋๋ค. ๋ด๋ถ ์ฝ๋๋ฅผ ๋ณด๊ฒ ์ต๋๋ค.


- path๋ JSON ๋ด์์์ ๊ฒฝ๋ก๋ฅผ ๋ปํฉ๋๋ค.
- type์ ๋ฌ์ฌ๋ ํ๋์ ํ์ (String, Number ๋ฑ)์ ๋ปํฉ๋๋ค.
- isOptional์ ํด๋น ํ๋๊ฐ ํ์์ ์ธ์ง, ์ต์ ์ธ์ง๋ฅผ ๋ปํฉ๋๋ค.
๊ทธ๋ฐ๋ฐ ์์ ํ ์คํธ ์ฝ๋๋ฅผ ๋ณด๋ฉด, description ์ฆ "์ค๋ช "์ด ์์ฑ๋์ด ์๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค. ๊ทธ๋ฐ๋ฐ ์ง๊ธ FieldDescriptor์์๋ ๋ณด์ด์ง ์์ต๋๋ค. ์ด๊ฒ์ FieldDescriptor > IgnorableDescriptor<FieldDescriptor> > AbstractDescriptor<T extends AbstractDescriptor<T>>์ ์์ต๋๋ค.

attributes๋ ํ๋ ํ ์ด๋ธ์ ๋ค์ด๊ฐ ์๋ ๊ฐ์ ์ปค์คํ ํ๊ฒ ๋ฐ๊ฟ ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์์ต๋๋ค. ์๋๋ ๊ทธ ์์์ ๋๋ค.
@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",
requestFields(
fieldWithPath("title").description("์ ๋ชฉ"),
fieldWithPath("content").description("๋ด์ฉ")
.attributes(
key("path").value("hello")
)
)
));
}
ํด๋น ํ ์คํธ ์ฝ๋๋ฅผ ์คํํ๋ฉด ์๋ก์ด ์ค๋ํซ์ธ request-fields.adoc์ด ์์ฑ๋ฉ๋๋ค.

๊ฒฐ๋ก ๋ฐ ํ์ฅ ์
์ด๋ฒ ๊ธ์ ๊ฐ๋จํ๊ฒ ๋ง๋ฌด๋ฆฌํ๋ ค ํฉ๋๋ค. ์ฌ๋ ค๋ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๋ฉด, ์์ ์ด ์ํ๋ ๋๋ก ์ฌ์กฐ์ ํ ์ ์์ต๋๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๋ ๊ฒ ์ค๋ํซ ํ์ผ๋ค์ด ์์ฑ๋๋ฉด, ์ํ๋ ์์๋ก adoc ํ์ผ๋ค์ ๋ฐฐ์นํ๋ฉด ๋ฉ๋๋ค.

์๋ ์์๋ ํ๋ก์ ํธ๋ฅผ ํ๋ฉด์ ์์ฑํ ํ ์คํธ ์ฝ๋์ธ๋ฐ์, ์์ ๋งํ attributes๋ฅผ ์ด์ฉํ์ง ์๋๋ค๋ฉด Path์ ๊ฒฝ์ฐ ๋ฐฐ์ด์ ์์์๋ hobbies[].hobby ์ด๋ฐ ์์ผ๋ก ๊น๋ํ์ง ์๊ฒ ํํ๋๋ ์ (์ด๋ฐ ์์ผ๋ก ์์ฑํ์ง ์์ ๊ฒฝ์ฐ ํ ์คํธ ์ ์์ธ๊ฐ ๋ฐ์ํฉ๋๋ค.)์ ํด๊ฒฐํ ์ ์์ต๋๋ค.

์ด๋ฅผ html๋ก ํํํ๋ฉด ์๋์ ๊ฐ์ด ๋์ต๋๋ค.

๋ค์ ๊ธ์ ์์ฑํ๋ค๋ฉด ํ์ฌ ๊ฒ์ํ JSON์ด ํ ์ค๋ก ํํ๋๋ ๋ฌธ์ ๊ฐ ์๋๋ฐ, ์ด๋ฅผ ๋ง์ง๋ง ์ฌ์ง์ฒ๋ผ ์ ๋ ฌ์์ผ ํํํ ์ ์๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
๋ง์ง๋ง ์ฃผ์์ฌํญ!
๋ง์ง๋ง์ผ๋ก ์๋ ค๋๋ฆด ๊ฒ์, ์กฐํ์ ๊ฒฝ์ฐ ํน์ ํ๋๋ฅผ ์์ฒญ์ ๋ด์ง ์๋ ๊ฒฝ์ฐ๊ฐ ๋ง์ต๋๋ค. ๊ทธ๋ ๊ธฐ์ ๊ฐ๋ ์์ฒญ adoc ํ์ผ์ ์์ ๋น์๋ฒ๋ฆด ๋๊ฐ ์์ ๊ฒ์ ๋๋ค.
๊ทธ๋ฌ๋ ์ด๋ ๊ฒ ๋๋ฉด ์ด๋ URL๋ก ์์ฒญ์ ํ๋์ง ์ ํ ๋ชจ๋ฅด๊ธฐ ๋๋ฌธ์, ์์ฒญ ํ๋๊ฐ ์๋๋ผ๋ http-request.adoc ํ์ผ์ ๊ผญ ๋ฃ์ด์ฃผ์๋ ๊ฒ ์ข์ต๋๋ค.
