๐Ÿช„ Swagger + RestDocs ๋ฌธ์„œํ™”

ํ…์ €๋ฆฐํ‹ฐยท2023๋…„ 11์›” 17์ผ
1

๐Ÿ›Ž๏ธย ์‚ฌ๊ฑด์˜ ๋ฐœ๋‹จ

์Šคํ”„๋ง ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ ๋ฌธ์„œํ™”๋ฅผ ํ•œ๋‹ค๊ณ  ํ•˜๋ฉด ์ผ๋‹จ RestDocs ๋‹ค.

์—ฌํƒœ๊ป RestDocs๋งŒ ์จ์™”๋‹ค.

Swagger ๋‹จ์ ์œผ๋กœ

  1. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ๋ฌธ์„œํ™” ์ฝ”๋“œ๊ฐ€ ํฌํ•จ๋จ
  2. RESTful API ์˜ ์ž๊ธฐ์ฆ๋ช…์„ ์ง€์›ํ•ด์ฃผ์ง€ ๋ชปํ•จ

์ด๋Ÿฐ ๋‹จ์ ์ด ์žˆ๊ธธ๋ž˜ RestDocs๋งŒ ์จ์™”๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ฒˆ ํŒ€ํ”Œ์—์„œ ํ”„๋ก ํŠธ์—์„œ Swagger ๋กœ ๋ฌธ์„œํ™” ์ž‘์—… ํ•ด์ค„ ๊ฒƒ์„ ์š”์ฒญํ–ˆ๋‹ค.

๋‹จ์ ์ด ์žˆ๋Š” ๊ฒƒ์„ ์•„๋Š”๋Œ€๋„ ๋‹จ์ˆœํžˆ ๋ณด๊ธฐ ์ข‹๊ณ , ์“ฐ๊ธฐ ํŽธํ•˜๋‹ค๊ณ  Swagger ๋กœ ๋„˜์–ด๊ฐ€๋Š” ๊ฒƒ์€ ์ข‹์€ ๋ฐฉ๋ฒ•์ด ์•„๋‹ˆ๋ผ ์ƒ๊ฐํ–ˆ๋‹ค.

๊ทธ ๋•Œ ํŒ€์› ์ค‘์— ํ•œ ๋ช…์ด Swagger ๋ž‘ RestDocs ๋ฅผ ์„ž์–ด์„œ ์“ฐ๋Š” ๋ฌธ์„œํ™” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค๊ณ  ์•Œ๋ ค์คฌ๋‹ค.

์•ˆ ์“ธ ์ด์œ ๊ฐ€ ์—†์—ˆ๋‹ค.

๐Ÿงฌย ๊ตฌ์กฐ

๐Ÿ”ฎย Swagger

Swagger ๋ฌธ์„œํ™” = SwaggerUI + Swagger

Swagger ์ฝ”๋“œ๋Š” openapi ์ฝ”๋“œ๋กœ ๋ณ€ํ™˜๋˜๊ณ , Swagger UI๊ฐ€ ์ด openapi ์ฝ”๋“œ๋ฅผ ์ด์šฉํ•ด์„œ ์‹œ๊ฐํ™”ํ•˜๋Š” ๋ฐฉ์‹

์•ž์„œ ์„ค๋ช…ํ•œ Swagger์˜ ๋‹จ์ ์€ ์œ„์˜ Swagger ์— ํ•ด๋‹นํ•˜๋Š” ์ด์•ผ๊ธฐ๋‹ค.

๋‹ค์‹œ ๋งํ•ด์„œ, ์šฐ๋ฆฌ๋Š” SwaggerUI ๋งŒ ์“ฐ๊ณ , Swagger ๋Š” ๋ฒ„๋ฆฌ๋ฉด ๋œ๋‹ค๋Š” ์ด์•ผ๊ธฐ

ํ๋ฆ„

Swagger์™€ RestDocs ๋ฅผ ํ•จ๊ป˜ ์“ด๋‹ค๋Š” ๊ฒƒ์€ ์ด๋Ÿฐ ๊ณผ์ •์œผ๋กœ ์ง„ํ–‰๋œ๋‹ค.

  1. RestDocs ๋กœ ๋ฌธ์„œํ™” ์Šค๋‹ˆํŽซ์„ ๋งŒ๋“ฌ
  2. ์Šค๋‹ˆํŽซ์„ openapi3 ์œผ๋กœ ๋ณ€ํ™˜
  3. Swagger UI์—์„œ openapi3 ๋ฅผ ํ™œ์šฉํ•ด html ๋กœ ํ‘œ์ถœ

ํ๋ฆ„ ๋น„๊ต

๐ŸŽย ํ•„์š”ํ•œ ๊ฑฐ

๊ทธ๋Ÿผ ํ•„์š”ํ•œ ๊ฒŒ ๋ฌด์—‡์ธ์ง€ ๋‚˜์˜จ๋‹ค.

  • Spring RestDocs
  • restdocs-api-spec
  • OpenAPI3
  • Swagger UI

์œ„ 4๊ฐœ์˜ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ด์„œ ๋ฌธ์„œํ™”๋ฅผ ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

๐Ÿ“’Spring RestDocs

์Šคํ”„๋ง์—์„œ ํ…Œ์ŠคํŠธ์™€ ๋ฌธ์„œํ™”๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค.

MockMvc, Rest Assured, WebTestClient ๊ฐ™์ด ๊ฐ์ž ํŠน์ƒ‰์žˆ๋Š” ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด์„œ ํ…Œ์ŠคํŠธ/๋ฌธ์„œํ™”๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌธ์„œํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜๋ฉด build ํŒจํ‚ค์ง€ ๋‚ด์— ์Šค๋‹ˆํŽซ์ด ๋งŒ๋“ค์–ด์ง€๊ฒŒ ๋˜๊ณ ,

์ด ์Šค๋‹ˆํŽซ์„ ํ™œ์šฉํ•ด์„œ asciidoctor ๊ฐ€ html ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋ Œ๋”๋งํ•˜๊ฒŒ ๋œ๋‹ค.

์žฅ๋‹จ์ 

์žฅ์ ์œผ๋กœ๋Š”

  1. ํ…Œ์ŠคํŠธ, ๋ฌธ์„œํ™”๋ฅผ ๋™์‹œ์— ์ˆ˜ํ–‰ ๊ฐ€๋Šฅ
  2. RESTful API ์˜ ์ž๊ฐ€์ฆ๋ช… ์กฐ๊ฑด ๋งŒ์กฑ ๊ฐ€๋Šฅ

๋‹จ์ ์œผ๋กœ๋Š”

  1. ์•ˆ ์˜ˆ์จ
  2. ์ง์ ‘ ์Šค๋‹ˆํŽซ์„ ์„ ํƒํ•ด์„œ html ํŒŒ์ผ์— ๋„ฃ์–ด์ค˜์•ผ ํ•จ

Gradle

์ฝ”๋“œ๋ฅผ ๊ฐ€์ ธ์˜ค๊ธด ํ–ˆ๋Š”๋ฐ

์Šคํ”„๋ง ๋ถ€ํŠธ ์‚ฌ์šฉํ•˜๋ฉด initializer ๋กœ ์ถ”๊ฐ€ํ•ด์„œ ํ•˜๋ฉด ํŽธํ•˜๋‹ค.

dependencies {
		// RestDocs
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

์ฝ”๋“œ

๋‚˜๋Š” MockMvc ๋กœ ๋ฌธ์„œํ™”๋ž‘ ํ…Œ์ŠคํŠธ๋ฅผ ํ–ˆ๋‹ค.

์•„๋ž˜ ๋ณด์ด๋Š” ๊ฒƒ์ฒ˜๋Ÿผ

  1. @AutoConfigureMockMvc, @AutoConfigureRestDocs ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ RestDocs MockMvc ํ…Œ์ŠคํŠธ ์„ค์ •์„ ํ•ด์ค€๋‹ค.
  2. org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders ๋ชจ๋“ˆ์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ์™€ ๋ฌธ์„œํ™” ์ˆ˜ํ–‰
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@SpringBootTest
@Transactional
class ReviewRestControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private TestConfig testConfig;

    @Test
    @DisplayName("๋ฆฌ๋ทฐ๋ฅผ ๋“ฑ๋กํ•œ ์  ์—†๋‹ค๋ฉด ๋ฆฌ๋ทฐ ๋“ฑ๋ก ๊ฐ€๋Šฅํ•จ์„ ์‘๋‹ตํ•œ๋‹ค.")
    void getReviewable_NotExistsReview_Reviewable() throws Exception {
        // Given
        final Long academyId = 1L;

        // When
        ResultActions perform = mockMvc.perform(get("/reviews/reviewable")
                .param("academyId", String.valueOf(academyId))
                .header(AUTHORIZATION_HEADER, BEARER + testConfig.getJwt())
                .accept(APPLICATION_JSON_VALUE)
                .contentType(APPLICATION_JSON_VALUE));

        // Then
        perform.andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().contentTypeCompatibleWith(APPLICATION_JSON_VALUE))
                .andExpect(jsonPath("$.academyId").value(1L))
                .andExpect(jsonPath("$.reviewable").value(true))
                .andDo(document("get-reviewable",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
				                requestHeaders(
				                        headerWithName("Authorization").description("JWT ํ† ํฐ (Bearer)")
				                ),
				                queryParameters(
				                        parameterWithName("academyId").description("ํ•™์› ์•„์ด๋””")
				                ),
				                responseFields(
				                        fieldWithPath("academyId").type(NUMBER).description("ํ•™์› ์•„์ด๋””"),
				                        fieldWithPath("reviewable").type(BOOLEAN).description("๋ฆฌ๋ทฐ ๋“ฑ๋ก ๊ฐ€๋Šฅ ์—ฌ๋ถ€")
				                )
                ));
    }

}

๐Ÿ“•ย restdocs-api-spec

Spring RestDocs ๋กœ ๋งŒ๋“ค์–ด์ง„ ์Šค๋‹ˆํŽซ์„ OpenAPI3 ์ŠคํŽ™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค.

์ฐธ๊ณ ๋กœ ์Šคํ”„๋ง ๋ฒ„์ „, ์Šคํ”„๋ง ๋ถ€ํŠธ ๋ฒ„์ „, RestDocs ๋ฒ„์ „, restdocs-api-spec ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋ฒ„์ „ ์ค‘ ์•ˆ ๋งž๋Š”๊ฒŒ ์žˆ์œผ๋ฉด openapi3 ์ŠคํŽ™์ด ๋งŒ๋“ค์–ด์ง€์ง€ ์•Š๋Š”๋‹ค.

Gradle

buildscript {
    ext {
        restdocsApiSpecVersion = '0.17.1'
    }
}

dependencies {
		// Spring Rest Api Spec
    testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"
}

๐Ÿ“—ย OpenAPI3

openapi3 ์ŠคํŽ™์„ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ์œ„ํ•ด์„œ ์ถ”๊ฐ€์ ์ธ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค.

Gradle

openapi3 {
    servers = [
            { url = "http://localhost:8080" },
            { url = "https://์„œ๋ฒ„url" }
    ]
    title = "ํ”Œ์  ์ œ๋ชฉ"
    description = "ํ”Œ์  ์„ค๋ช…"
    version = "๋ฒ„์ „"
    format = "json" // (json / yaml)
    outputDirectory = "src/main/resources/static"
    outputFileNamePrefix = "swagger"
}

Gradle openapi3 ํƒœ์Šคํฌ๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.

  • servers
    • ์ด๋ ‡๊ฒŒ ๋ฐฐ์—ด ํ˜•์‹์œผ๋กœ ํ—ˆ์šฉํ•˜๋ ค๋Š” ์„œ๋ฒ„ ๋„๋ฉ”์ธ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
    • server = โ€œurlโ€ ์ด๋ ‡๊ฒŒ ํ•œ ์„œ๋ฒ„๋งŒ ์„ค์ •ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.
  • format
    • ํ˜•์‹์€ json๊ณผ yml ์ค‘์— ์„ ํƒํ•  ์ˆ˜ ์žˆ๋‹ค.
  • outputDirectory
    • openAPI3 ์ŠคํŽ™ ๊ฒฐ๊ณผ๊ฐ€ ์ €์žฅ๋˜๋Š” ์ถœ๋ ฅ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค.
  • outputFileNamePrefix
    • openAPI3 ์ŠคํŽ™ ๊ฒฐ๊ณผ ํŒŒ์ผ์˜ ํŒŒ์ผ๋ช… prefix๋‹ค.

๐Ÿ“˜ย Swagger UI

์ด์ œ ๋งŒ๋“ค์–ด์ง„ openAPI3 ์ŠคํŽ™์„ Swagger UI ๋ฅผ ํ†ตํ•ด HTML ๋กœ ํ‘œ์ถœํ•ด์ฃผ์ž.

Gradle

springfox ๊ทธ๋ฃน๊บผ ์“ฐ๋Š” ์‚ฌ๋žŒ ์žˆ๋Š”๋ฐ, ๊ทธ ์˜์กด์„ฑ ์“ธ ๊ฑฐ๋ฉด ๋ฒ„์ „ 3์ด์ƒ์€ ์จ์•ผ ํ•จ

// Swagger UI
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

YAML ์„ค์ •

์ฝ˜ํ…ํŠธ ํƒ€์ž…์„ json์œผ๋กœ ์„ค์ •ํ–ˆ๋‹ค.

์œ„์— openapi3 ์„ค์ •์—์„œ json์œผ๋กœ ์„ค์ •ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

swagger UI์— ๋Œ€ํ•œ ๊ธฐ๋ณธ url ์„ ์„ค์ •ํ•˜๊ณ , ์ด url์— ๋Œ€ํ•œ ์ ‘๊ทผ url์„ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

springdoc:
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    url: /v3/api-docs
    path: /docs/swagger

๐Ÿ”–ย ์ฝ”๋“œ ์ž‘์„ฑ๋ฒ•

๊ธฐ๋ณธ์ ์œผ๋กœ ์œ„์— ์„ค๋ช…ํ•œ RestDocs ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

๋ณ€๊ฒฝ์ ์ด ๊ฑฐ์˜ ์—†๊ธฐ ๋•Œ๋ฌธ.

  1. ๋ฌธ์„œํ™” ํด๋ž˜์Šค ๋ณ€๊ฒฝ

    // Before
    import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
    
    // After
    import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
  2. ๋ฌธ์„œํ™” ์ฝ”๋“œ ์ˆ˜์ • (ํ•„์ˆ˜ ์•„๋‹˜; ์•ˆ ํ•ด๋„ ๋Œ์•„๊ฐ)

    // Before
    .andDo(document("post-like",
            preprocessRequest(prettyPrint()),
            preprocessResponse(prettyPrint()),
            requestHeaders(
                    headerWithName("Authorization").description("JWT ํ† ํฐ (Bearer)")
            ),
            requestFields(
                    fieldWithPath("academyId").type(NUMBER).description("ํ•™์› ์•„์ด๋””")
            ),
            responseFields(
                    fieldWithPath("likeId").type(NUMBER).description("์ข‹์•„์š” ์•„์ด๋””"),
                    fieldWithPath("memberId").type(NUMBER).description("ํ•™์› ์•„์ด๋””"),
                    fieldWithPath("academyId").type(NUMBER).description("ํ•™์› ์•„์ด๋””")
            )
    ));
    
    // After
    .andDo(document("post-like",
            preprocessRequest(prettyPrint()),
            preprocessResponse(prettyPrint()),
            resource(ResourceSnippetParameters.builder()
                    .tag(TAG)
                    .summary("์ข‹์•„์š” ๋“ฑ๋ก")
                    .requestHeaders(
                            headerWithName("Authorization").description("JWT ํ† ํฐ (Bearer)")
                    )
                    .requestFields(
                            fieldWithPath("academyId").type(NUMBER).description("ํ•™์› ์•„์ด๋””")
                    )
                    .responseFields(
                            fieldWithPath("likeId").type(NUMBER).description("์ข‹์•„์š” ์•„์ด๋””"),
                            fieldWithPath("memberId").type(NUMBER).description("ํ•™์› ์•„์ด๋””"),
                            fieldWithPath("academyId").type(NUMBER).description("ํ•™์› ์•„์ด๋””")
                    )
                    .build()
            )
    ));

๐Ÿงฎย ๊ฒฐ๊ณผ ํ™•์ธ

์„ค์ •ํ•œ swagger UI url ๊ฒฝ๋กœ๋กœ ์ ‘๊ทผํ•˜๋ฉด ๋œ๋‹ค.

http(s)://๋„๋ฉ”์ธ/docs/swagger

http://localhost:8080/docs/swagger

์œ„ yml ํŒŒ์ผ์—์„œ ์„ค์ •ํ•œ ๊ฒƒ์ฒ˜๋Ÿผ ํ•ด๋‹น url๋กœ ์ ‘๊ทผํ•˜๋ฉด swagger ui ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š” url๋กœ ์—ฐ๊ฒฐ๋œ๋‹ค.

profile
๊ฐœ๋ฐœํ•˜๊ณ  ๋งํ…Œ์•ผ

0๊ฐœ์˜ ๋Œ“๊ธ€