Spring Rest Doc - low-hill/Knowledge GitHub Wiki

  • ์‹ ๋ขฐ๋„๊ฐ€ ๋†’์€ document๋ฅผ ์ƒ์„ฑํ•ด์ฃผ๋Š” RESTful ์„œ๋น„์Šค์˜ ๋ฌธ์„œํ™” ๋„๊ตฌ. ๊ธฐ๋ณธ์ ์œผ๋กœ Asciidoctor๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ HTML๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ํ•„์š” ํ•œ ๊ฒฝ์šฐ Markdown์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค.

์žฅ์ 

  • Spring MVC test framework, Spring WebFlux WebTestClient, REST ASSured 3๋ฅผ ํ†ตํ•ด ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•ด์•ผ ๋ฌธ์„œ ์ž‘์„ฑ ๋จ.
    • API ์‹ ๋ขฐ๋„๋ฅผ ๋†’์ด๊ณ  ๋”๋ถˆ์–ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์˜ ๊ฒ€์ฆ์„ ๊ฐ•์ œ๋กœ ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฌธ์„œํ™” ๋„๊ตฌ
  • ์‹ค์ œ ์ฝ”๋“œ์— ์ถ”๊ฐ€๋˜๋Š” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค.
    • ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌ๋˜์–ด Swagger๊ฐ™์ด Config์„ค์ • ์ฝ”๋“œ๋‚˜ ์–ด๋…ธํ…Œ์ด์…˜์ด ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์— ์ถ”๊ฐ€๋  ์ผ์ด ์—†๋‹ค.

Spring Rest Docs Flow

  • Test โ†’ Snippets โ†’ Template โ†’ Document
    • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ./build/generated-snippetsํด๋”์—REST docs snippets ํŒŒ์ผ๋“ค์ด ์ƒ์„ฑ
      • curl-request
      • http-request
      • http-response
      • httpie-request
      • request-body
      • response-body
    • ์ƒ์„ฑ๋œ ํŒŒ์ผ๋“ค์„ ์˜๋„ํ•˜๋Š” ๋Œ€๋กœ ํ…œํ”Œ๋ฆฟ์— ๋ฐฐ์น˜ ํ…œํ”Œ๋ฆฟ์„ ๊ธฐ์ค€์œผ๋กœ html ํŒŒ์ผ์„ ์ƒ์„ฑ

Minimum requirements

  • Java 8
  • Spring Framework 5( 5.0.2 or later)

์ž‘์—…ํ™˜๊ฒฝ

  • Java 11
  • Spring boot 2.7.4
  • MockMvc
  • AsciiDoc

gradle ์˜์กด์„ฑ ์ฃผ์ž…

  • ./build.gradle ์„ค์ •
plugins { ...
    id 'org.asciidoctor.jvm.convert' version '3.3.2'
        ...
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
    mavenCentral()
}
ext {
    set('snippetsDir', file("build/generated-snippets"))    //
}
configurations {
    asciidoctorExtensions
}
dependencies {
        ...
    implementation 'org.springframework.boot:spring-boot-starter-web'
    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
...
}
tasks.named('asciidoctor') {
    inputs.dir snippetsDir
    dependsOn test
    configurations 'asciidoctorExtensions'  //
}
bootJar {
    dependsOn asciidoctor
}
  • gradle7์—์„œ org.asciidoctor.convert(v 1.5.9) ํ”Œ๋Ÿฌ๊ทธ์ธ์ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ์œ„์™€ ๊ฐ™์€ ์„ค์ •์œผ๋กœ ์‚ฌ์šฉ -> Asciidoctor ๋นŒ๋“œ ์˜ค๋ฅ˜ ์ฐธ๊ณ 

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ

@Test
public void findById() throws Exception {
    User userRes = new User(1L, "u1", "[email protected]");
    this.mockMvc.perform(RestDocumentationRequestBuilders.get("/user/{userId}", userRes.getId()))
            .andExpect(status().isOk())
            .andDo(document("user-get-one",
                    pathParameters(
                            parameterWithName("userId").description("")
                    ),
                    responseFields(
                            fieldWithPath("data").description(""),
                            fieldWithPath("data.id").type(JsonFieldType.NUMBER).description(""),
                            fieldWithPath("data.name").type(JsonFieldType.STRING).description(""),
                            fieldWithPath("data.email").type(JsonFieldType.STRING).description("")
                    )
             ));
}

document ์ž‘์„ฑ ๋ฐฉ๋ฒ•

  • RestDocumentationRequestBuilders๋กœ API ํ˜ธ์ถœ
  • path ํŒŒ๋ผ๋ฏธํ„ฐ
pathParameters(
  parameterWithName("userId").description("์•„์ด๋””") 
)
  • Request body
requestFields(
                fieldWithPath("id").description("")
)
  • Json Response
responseFields( fieldWithPath("data").description("๊ฒฐ๊ณผ๋ฉ”์‹œ์ง€")
)
  • response ์ ‘๊ทผ ๋ฐฉ๋ฒ•
    • ๋ฐฐ์—ด์ผ๋•Œ [].์œผ๋กœ ์ ‘๊ทผ
      • fieldWithPath("[].id").type(JsonFieldType.STRING).description("์ƒ์„ธ ์„ค๋ช…...")
    • ๋ฐฐ์—ด์— ์ด๋ฆ„์ด ์žˆ์„๋•Œ *.[]๋กœ ์ ‘๊ทผ
      • fieldWithPath("data.[].id").type(JsonFieldType.STRING).description("์ƒ์„ธ ์„ค๋ช…...")
  • responsefield example
this.mockMvc.perform(get("/user"))
                ...
                .andDo(document("user-get-all",
                        responseFields(
                                fieldWithPath("data").description(""),
                                fieldWithPath("data.[].id").type(JsonFieldType.NUMBER).description(""),
                                fieldWithPath("data.[].name").type(JsonFieldType.STRING).description(""),
                                fieldWithPath("data.[].email").type(JsonFieldType.STRING).description("")

  • test ์ˆ˜ํ–‰ ์‹œ ./build/generated-snippets/ ํ•˜์œ„์— ๋ฌธ์„œ(*.adoc)๊ฐ€ ์ƒ์„ฑ ๋œ๋‹ค.

snippets ์—ฐ๊ฒฐ ๋ฐ ๋ฌธ์„œํ™”

์ƒ์„ฑ๋œ snippets ํŒŒ์ผ๋“ค์„ ์—ฐ๊ฒฐํ•ด์ค„ ์‚ฌ์šฉ์ž ์ •์˜ .adocํŒŒ์ผ์„ "src/docs/aciidocs"๊ฒฝ๋กœ์— ์ƒ์„ฑํ•˜๊ณ  ๋ฌธ์„œ๋ฅผ ๊ตฌ์„ฑ ํ•œ๋‹ค.

  • snippets ํŒŒ์ผ ์—ฐ๊ฒฐ
  • include๋กœ ๊ฐ๊ฐ์˜ ํŒŒ์ผ ์‚ฝ์ž…ํ•˜๋Š” ๋Œ€์‹  operation์œผ๋กœ ์›ํ•˜๋Š” ํŒŒ์ผ๋งŒ ๊ฐ€์ ธ์˜ด
[resources_user](/low-hill/Knowledge/wiki/resources_user)
== User Sample
[resources_user_list](/low-hill/Knowledge/wiki/resources_user_list)
=== User
`GET`
operation::user-get-all[snippets='response-fields,http-response']
[resources_user_get_one](/low-hill/Knowledge/wiki/resources_user_get_one)
=== User
operation::user-get-one[snippets='path-parameters,response-fields,http-response']
  • ์ž‘์„ฑ ํ›„ build ์‹œ build/docs/asciidoc์— hmtl ํŒŒ์ผ์ด ์ƒ์„ฑ ๋œ๋‹ค.
Source files Generated files
src/docs/asciidoc/*.adoc build/docs/asciidoc/*.html