๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿš€ ํŒ (๊ธฐ์ˆ  ์ ์šฉ ๋ฐฉ๋ฒ• ๋“ฑ)/๐ŸŒฑ Spring

[Spring REST Docs โœ๏ธ] ์–ด๋ ต๊ฒŒ๋งŒ ๋Š๊ปด์กŒ๋˜ REST Docs๋ฅผ ์ ์šฉํ•ด๋ณด์ž! (1)

by dev_writer 2024. 2. 7.

Spring REST Docs

์˜ค๋Š˜์€ ์Šคํ”„๋ง REST Docs์— ๋Œ€ํ•ด ์ ์šฉํ•˜๊ณ , ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค. ์Šคํ”„๋ง์„ ์–ด๋Š ์ •๋„ ์จ ๋ณด๊ณ , ํ…Œ์ŠคํŠธ ๋ฐ ๋ฌธ์„œํ™”๋“ฑ์„ ํ•ด ๋ณด์‹  ๋ถ„๋“ค์ด๋ผ๋ฉด ์Šค์›จ๊ฑฐ๋‚˜ ๋…ธ์…˜, ํฌ์ŠคํŠธ๋งจ์œผ๋กœ API ๋ฌธ์„œ๋ฅผ ๋งŒ๋“  ๊ฒฝํ—˜์ด ์žˆ์œผ์‹ค ํ…๋ฐ์š”, ์Šคํ”„๋ง์— ์žˆ๋Š” REST Docs๋Š” ํ˜„์—…์—์„œ ๋งŽ์ด ์“ฐ๋Š” ๋ฌธ์„œํ™” ์Šคํ‚ฌ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ธฐ์กด ๋ฌธ์„œํ™” ๋„๊ตฌ๋“ค์€ ์Šค์›จ๊ฑฐ์˜ ๊ฒฝ์šฐ ์Šค์›จ๊ฑฐ๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ๊ฐ€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฌป๋Š”๋‹ค๋Š” ์ , ๊ทธ๋ฆฌ๊ณ  ์ˆ˜์ • ์‹œ ์—…๋ฐ์ดํŠธ ์ƒํƒœ์™€ ๋ฌธ์„œ์˜ ์ƒํƒœ๊ฐ€ ์„œ๋กœ ๋งž์ง€ ์•Š๋Š” ๋ฌธ์ œ ๋“ฑ์ด ์žˆ์—ˆ๋Š”๋ฐ์š”, ์Šคํ”„๋ง REST Docs๋Š” ํ…Œ์ŠคํŠธ, ๋นŒ๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž‘์„ฑํ•˜๊ธฐ์— ์‹ฑํฌ๊ฐ€ ์œ ์ง€๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

์•„๋งˆ ์‹œ์ž‘ํ•˜๋Š” ๋‹จ๊ณ„์—์„œ๋ถ€ํ„ฐ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•ด์•ผ ํ• ์ง€ ๊ฐ์ด ์˜ค์‹œ์ง€ ์•Š๋Š” ๋ถ„๋“ค์ด ๋งŽ์„ ๊ฒƒ ๊ฐ™์•„, ์ œ๊ฐ€ ํ•ด๊ฒฐํ•œ ๊ณผ์ •์„ ๋‹จ๊ณ„์ ์œผ๋กœ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค!

 

1. build.gradle์— ํ•„์š”ํ•œ ์ฝ”๋“œ ์ž‘์„ฑ

๊ธฐ์ˆ ์„ ์ ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์Šคํ”„๋ง REST Docs์™€ ๊ด€๋ จ๋œ ๋‚ด์šฉ๋“ค์„ build.gradle์— ์ž‘์„ฑํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด๋ด…์‹œ๋‹ค.

๊ณต์‹ ๋ฌธ์„œ์— ์žˆ๋Š” 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)์„ ์˜๋ฏธํ•จ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

String outputDirectory๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค!

 

๊ทธ๋Ÿฐ๋ฐ ์ฒ˜์Œ์— build.gradle์—์„œ ์–ด๋–ค ๊ฒƒ์„ ์ž‘์„ฑํ–ˆ์—ˆ๋Š”์ง€ ๋‹ค์‹œ ์ƒ๊ฐํ•ด ๋ด…์‹œ๋‹ค. ์Šค๋‹ˆํŽซ (snipptes)์— ๋Œ€ํ•ด ์ž๋™์œผ๋กœ ๋ถˆ๋Ÿฌ์˜ค๊ณ  (spring-restdocs-asciidoctor), ์‹ฌ์ง€์–ด ๊ฒฝ๋กœ๋ฅผ ๋ช…์‹œํ•˜๊ธฐ๊นŒ์ง€ ํ–ˆ์Šต๋‹ˆ๋‹ค. (snippetsDir = file('build/generated-snippets'))

 

๋”ฐ๋ผ์„œ ์ด๋Ÿฌํ•œ ๋‚ด๋ถ€ ๊ณผ์ •์„ ์ž๋™์œผ๋กœ ํ•ด ์ฃผ๋Š” ๊ฒŒ ๋ฐ”๋กœ @AutoConfigureRestDocs์ž…๋‹ˆ๋‹ค. ์ง์ ‘ ์ž‘์„ฑํ•  ๋•Œ ๋ณด๋‹ค ํ•„์š”ํ•œ ์ฝ”๋“œ ์–‘์ด ์ค„์–ด๋“œ๋‹ˆ ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ํ•ฉ์‹œ๋‹ค!

 

document("...")

๋”๋ณด๊ธฐ
์กฐ๊ธˆ ๋งŽ์ด ๋œฏ์–ด๋ณด๊ฒŒ ๋ฉ๋‹ˆ๋‹ค! ์•„๋ž˜์—๋„ ์ž‘์„ฑํ–ˆ๋“ฏ ๊ฒฐ๋ก  ๋จผ์ € ๋ง์”€๋“œ๋ฆฌ๋ฉด ๋ถ™์ธ ๋ฌธ์ž์—ด์„ ๊ฒฝ๋กœ๋กœ ํ•˜์—ฌ adoc ํŒŒ์ผ๋“ค์„ ์ž‘์„ฑํ•œ๋‹ค๋Š” ๊ฒƒ์œผ๋กœ ์ดํ•ดํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค :)

๋‹ค์Œ์œผ๋กœ๋Š” document("...")๋ฅผ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๊ฒฐ๋ก  ๋จผ์ € ๋ง์”€๋“œ๋ฆฌ๋ฉด, {์Šค๋‹ˆํŽซ ๊ฒฝ๋กœ}/{document ์•ˆ์— ์ž‘์„ฑํ•œ ๊ฒฝ๋กœ}์— adoc ํŒŒ์ผ๋“ค์ด ์ž‘์„ฑ๋ฉ๋‹ˆ๋‹ค.

build/generated-snippets/boards/save์— adoc ํŒŒ์ผ๋“ค์ด ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด๊ฒƒ์€ 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 ๋“ฑ)๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๋””๋ฒ„๊น…ํ•˜์—ฌ ๋‚ด๋ถ€ ์ •๋ณด๋ฅผ ๋œฏ์–ด๋ณธ ๊ฒฐ๊ณผ

 

attribute ์•ˆ์— ์žˆ๋Š” RestDocumentationContext๋Š” ์œ„์˜ build.gradle ๋ถ€๋ถ„์—์„œ ๋งํ–ˆ๋“ฏ์ด REST API๊ฐ€ ์ˆ˜ํ–‰๋œ ์ปจํ…์ŠคํŠธ๋ฅผ ์บก์Šํ™”ํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ผ๊ณ  ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์ธ ์Šค๋‹ˆํŽซ ๊ฒฝ๋กœ (build/generated-snippets)๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

outputDirectory๋ฅผ ๋ด์ฃผ์„ธ์š”!

 

operation์€ Snippet์ด ์ˆ˜ํ–‰ํ•  ๋‚ด์šฉ์„ ์ „์ฒด์ ์œผ๋กœ ๋‹ด๊ณ  ์žˆ๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. ์•ž์„œ "boards/save"๋ฅผ ์ž‘์„ฑํ•œ ๊ฒƒ์€, ์ด operation์˜ ์ด๋ฆ„ (name)์„ ๋œปํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

StandardOperationRequest, StandardOperationResponse ๋“ฑ๊ณผ ํ•จ๊ป˜ ์ €ํฌ๊ฐ€ ์ž‘์„ฑํ•œ ๊ฒฝ๋กœ๋ฅผ name์œผ๋กœ ๊ฐ€์ง‘๋‹ˆ๋‹ค.

 

this.getSnippets๋ฅผ ๋ณด๋ฉด ์™œ adoc ํŒŒ์ผ๋“ค์ด curl-request.adoc, http-request.adoc, http-response.adoc ๋“ฑ ์ด 6๊ฐœ๋กœ ์ƒ์„ฑ๋˜๋Š”์ง€๋„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ ์ด์œ ๋Š” org.springframework.restdocs.defaultSnippets์— 6๊ฐœ์˜ snippetName์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

getSnippets ์ž‘์—…์ด ๋๋‚˜๋ฉด ๊ฐ๊ฐ ๊ฐ€์ ธ์˜จ Snippet๋“ค์˜ document ์ž‘์—…์„ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. Snippet์€ ์•„๋ž˜์ฒ˜๋Ÿผ 25๊ฐ€์ง€์˜ ๊ตฌํ˜„ ์ฝ”๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ document๋ผ๋Š” ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

CurlRequestSnippet์€ TemplatedSnippet์„ ์ƒ์†๋ฐ›์œผ๋ฉฐ, ์ด TemplatedSnippet์€ Snippet์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. curl-request.adoc๊ณผ ๊ด€๋ จ ์žˆ๋Š” ์Šค๋‹ˆํŽซ์ž…๋‹ˆ๋‹ค.

TemplatedSnippet์˜ document ์ฝ”๋“œ

 

๋ฐ”๋กœ ์œ„์—์„œ 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์ž…๋‹ˆ๋‹ค.

 

resolveFile์„ ๋ณผ๊นŒ์š”? ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ƒ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๊ฐ€ ์•„๋‹ˆ๋ƒ์— ๋”ฐ๋ผ ๋งŒ๋“ค์–ด์ง€๋Š” ํŒŒ์ผ์ด ๋‹ฌ๋ผ์ง์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €ํฌ๋Š” ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•˜๊ธฐ ๋•Œ๋ฌธ์— (์ธ์ž๋กœ ๋ฐ›์€ outputDirectory๋Š” boards/save์ž…๋‹ˆ๋‹ค.) makeRelativeToConfiguredOutputDir๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

 

File์˜ ์ƒ์„ฑ์ž๋Š” ์•„๋ž˜์ฒ˜๋Ÿผ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ ์ธ์ž๋ฅผ ๋ถ€๋ชจ๋กœ ๋ฐ›๊ณ , ๋‘ ๋ฒˆ์งธ ์ธ์ž๋ฅผ ์ž์‹์œผ๋กœ ํ•˜๋Š”๊ตฐ์š”! ๊ทธ๋ž˜์„œ ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ๋ถ€๋ชจ/์ž์‹์œผ๋กœ ํ•จ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์š”์•ฝํ•˜์ž๋ฉด

  1. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๊ฒฝ๋กœ์™€ ํ•จ๊ป˜ document๋ฅผ ํ˜ธ์ถœ
  2. MockMvcRestDocumentation์—์„œ RestDocumentationGenerator ์ƒ์„ฑ, ์ž‘์„ฑํ•œ ๊ฒฝ๋กœ๋ฅผ identifier๋กœ ์ทจํ•จ
  3. RestDocumentationGenerator์—์„œ handle ๋ฉ”์„œ๋“œ ์‹คํ–‰
  4. ์ž‘์„ฑํ•œ ๊ฒฝ๋กœ, ์š”์ฒญ ์ •๋ณด, ์‘๋‹ต ์ •๋ณด, ๊ธฐํƒ€ ์„ค์ • ์ •๋ณด ์•ˆ์— ์žˆ๋Š” ์†์„ฑ๋“ค (RestDocumentationContext, MockHttpServletRequest ๋“ฑ)์„ ๊ฐ€์ง„ Operation์„ StandardOperation์œผ๋กœ ์ƒ์„ฑ
  5. ์†์„ฑ ์•ˆ์— ์žˆ๋˜ defaultSnippets๋“ค๋กœ๋ถ€ํ„ฐ Snippet ๋ชฉ๋ก ์ทจํ•จ
  6. ๊ฐ Snippet๋งˆ๋‹ค์˜ document ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ (Operation์„ ์ธ์ž๋กœ ๋„˜๊น€)
  7. TemplatedSnippet์ด Snippet์„ ๊ตฌํ˜„ํ•˜๊ณ , ํ•˜์œ„ Snippet๋“ค์€ ์ „๋ถ€ TemplatedSnippet์„ ์ƒ์†๋ฐ›์Œ
  8. ๊ฒฐ๊ณผ์ ์œผ๋กœ TemplatedSnippet์˜ document๊ฐ€ ์‹คํ–‰๋จ, ์ธ์ž๋กœ ๋ฐ›์€ Operation์— ์žˆ๋Š” RestDocumentationContext๋ฅผ ๋ฐ›๊ณ , ๋ฐ›์€ operation์˜ ์ด๋ฆ„ (์ž‘์„ฑํ•œ identifier)๊ณผ RestDocumentationContext์— ์žˆ๋Š” outputDirectory(build/generated-snippets)๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ข… ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•จ (build/generated-snippets/boards/save)
  9. ๊ฐ ์Šค๋‹ˆํŽซ ํŒŒ์ผ์ด ์ž‘์„ฑ๋จ

๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

 

๊ทธ๋ž˜์„œ ์ด๊ฒƒ๋“ค์„ ์–ด๋–ป๊ฒŒ ์“ธ ์ˆ˜ ์žˆ๋‚˜?

์œ„์˜ 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 ํŒŒ์ผ์ด ๋ณด์ด๊ธด ํ•ฉ๋‹ˆ๋‹ค. ๋‹จ, ๋นŒ๋“œ ํŒจํ‚ค์ง€์—๋งŒ ๋ณด์ž…๋‹ˆ๋‹ค.

build ์•ˆ์—๋Š” html์ด ๋ณด์ž…๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ src/main/resources/static/docs์—๋Š” ๋ณด์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

๋‹ค๋ฅธ ๋ถ„๊ป˜์„œ ์ž‘์„ฑํ•˜์‹  ๊ธ€์„ ์ฐธ๊ณ ํ•˜๋‹ˆ, 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์— ๋ณต์‚ฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

build ์•ˆ์— ์žˆ๋˜ html์ด src/main/resources/static/docs ์•ˆ์—๋„ ์ƒ๊ธฐ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

 

์‹ค์ œ๋กœ html ํŒŒ์ผ์„ ํ™•์ธํ•˜๋ ค๋ฉด ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‹คํ–‰ํ•œ ๋’ค localhost:8080/docs/board.html ๋“ฑ์œผ๋กœ ์ ‘๊ทผํ•˜์‹œ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์‹ค์ œ localhost์—์„œ ์ ‘์†ํ•œ ๊ฒฐ๊ณผ

 

๊ฒฐ๋ก 

์—ฌ๊ธฐ๊นŒ์ง€ REST Docs์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ํ™˜๊ฒฝ์„ค์ •๊ณผ document์— ๋Œ€ํ•œ ์›๋ฆฌ, html ํŒŒ์ผ ํ™•์ธ ๋“ฑ์„ ์•Œ์•„๋ดค์Šต๋‹ˆ๋‹ค.

 

์ด์ œ ๋‚จ์€ ๊ฒƒ์€ ์–ผ๋งˆ๋‚˜ ๋” ๋ฌธ์„œํ™”๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ํ•˜๊ณ , MockMvc์—์„œ ๋ฌธ์„œํ™”์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋“ฑ์ด ๋‚จ์•˜๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์€ ์•„์ง ์ดˆ๋ณด๋‹ค ๋ณด๋‹ˆ ๋” ์ตํžŒ ๋’ค์— ์ •๋ฆฌํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

 

ํ˜น์—ฌ๋‚˜ ์ž˜๋ชป๋œ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค!

Reference


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 ํŒจํ‚ค์ง€๋ฅผ ๋งŒ๋“ค์–ด๋†”์•ผ ํ•ฉ๋‹ˆ๋‹ค.