๐คจ RestDocs์ Swagger์ ์ฅ์ ๊ณผ ๋จ์
๋ฐ๋ธ์ฝ์ค ์ต์ข ํ๋ก์ ํธ๋ฅผ ์ค๋นํ๋ฉด์ ์ด๋ฒ ํ๋ก์ ํธ ์งํ์ค์ API ๋ฌธ์ํ๋ฅผ RestDocs๋ก ์งํํ ์ง Swagger๋ก ์งํํ ์ง ๊ณ ๋ฏผ์ค์ธ ์ํ์์ต๋๋ค.
์ด์ ์ Spring RestDocs๋ ์ฌ์ฉํด๋ณด์์ง๋ง Swagger๋ ์ฌ์ฉํด๋ณธ์ ์ด ์์๊ณ Swagger์ ๋ํด์ ๋๊ฐ ์๊ธฐ๋ก๋ ํ์ด์ง์์ ํ ์คํธํ ์ ์๋ ๊ธฐ๋ฅ์ ์ ๊ณตํด ํ๋ก ํธ์๋์ ์ข ๋ ์นํ์ ์ด๋ผ๋ ์ด์ผ๊ธฐ๋ฅผ ๋ค์์ต๋๋ค.
์ด์ API ๋ฌธ์ํ๋ฅผ ํ๊ธฐ ์ด์ ์ ์ด๋ค์ ์ฅ์ ๊ณผ ๋จ์ ์ ๋ํด์ ๋ค์๊ณผ ๊ฐ์ด ์ ๋ฆฌํ์ต๋๋ค.
- Spring RestDocs
์ฅ์ | ๋จ์ |
ํ ์คํธ ๊ธฐ๋ฐ์ผ๋ก ์คํ๋๊ธฐ ๋๋ฌธ์ ์ ๋ขฐ์ฑ์ด ๋์ | ์ถ๊ฐ์ ์ผ๋ก ์์ฑํด์ผ ํ๋ ํ ์คํธ ์ฝ๋๊ฐ ๋ง์ |
ํ๋ก๋์ ์ฝ๋์ ์ํฅ์ ์ฃผ์ง ์์ ๊น๋ํจ | API ํ ์คํธ ๊ธฐ๋ฅ์ด ์์ |
- Swagger
์ฅ์ | ๋จ์ |
API ํ ์คํธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๊ธฐ ๋๋ฌธ์ API๋ฅผ ์ดํดํ๋๋ฐ ๋์์ ์ค | ์ค์ ์ด ํ๋ก๋์ ์ฝ๋์ ์ถ๊ฐ๋๊ธฐ ๋๋ฌธ์ ํ๋ก๋์ ์ฝ๋์ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง |
ํ ์คํธ ์ฝ๋๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ์์ฑํ์ง ์์๋ ๋จ | ํ ์คํธ ๊ธฐ๋ฐ์ด ์๋๊ธฐ ๋๋ฌธ์ ๋ฌธ์์ ์ ๋ขฐ๋๊ฐ ๋จ์ด์ง |
๊ฐ์ฒด์ ๋ํ ์ ๋ณด๋ ์ถ๊ฐ ์์ฑ ๊ฐ๋ฅํจ | ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ๋ฌด๊ฑฐ์ |
๋ฐฑ์๋๋ง ์์ ํ๋ฉด Spring RestDocs๋ก ์ถฉ๋ถํ ํ ์ง๋ง ํ๋ก ํธ์ ๊ฐ์ด ํ์ ํ๋ฉด์ ํ๋ก์ ํธ๋ฅผ ์งํํ๊ธฐ ๋๋ฌธ์ API ํ ์คํธ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ Swagger๊ฐ ๋ ์ข๋ค๊ณ ํ๋จํ์ต๋๋ค.
ํ์ง๋ง Swagger์ ๊ฒฝ์ฐ ํ ์คํธ ๊ธฐ๋ฐ์ผ๋ก ์์ฑ๋๋ ๊ฒ์ด ์๋๊ธฐ ๋๋ฌธ์ ์ ๋ขฐ์ฑ์ด ๋จ์ด์ง๋ค๋ ๋จ์ ์ด ์์ด ์ด๋ฅผ ํด๊ฒฐํ ์ ์๋ ๋ฐฉ์์ ๋ํด ์ฐพ์๋ณด๊ฒ ๋์๊ณ OpenApi spec์ ์ด์ฉํ์ฌ RestDocs๋ก ์์ฑํ ๊ฒ์ Swagger๋ก ๋ณํ ์ํฌ ์ ์๋ค๋ ๊ฒ์ ์๊ฒ ๋์์ต๋๋ค.
โ ๏ธ OpenApi Spec
OpenApi Spec์ ์ด์ฉํ์ฌ Restdocs๋ก ์์ฑํ ํ ์คํธ ์ฝ๋๋ฅผ Swagger๋ก ๋ณํ์ํฌ ์ ์๋ค๋ ์ฌ์ค์ ์ฒดํฌํ์ผ๋ OpenApi Spec์ด๋ผ๋๊ฒ ๋ฌด์์ผ๊น์?
๋จผ์ Open Api์ OpenApi๋ ์๋ก ๋ค๋ฅด๋ค๋ ์ ์ ์์์ผํฉ๋๋ค.
Open Api์ ๊ฒฝ์ฐ ๊ฐ๋ฐฉ๋ API๋ผ๋ ์๋ฏธ๋ก ๋๊ตฌ๋์ง ์ฌ์ฉํ ์ ์๋๋ก ์๋ํฌ์ธํธ๊ฐ ๊ฐ๋ฐฉ๋ ์ํ์ API๋ฅผ ์๋ฏธํฉ๋๋ค.
๊ณต๊ณต๋ฐ์ดํฐํฌํธ์ ๋ค์ด๊ฐ๋ฉด ํ์ธํ ์ ์๋ API๋ค์ด Open API๋ผ๊ณ ํ ์ ์์ต๋๋ค.
๋ฐ๋ฉด์ OpenApi์ ๊ฒฝ์ฐ OAS(OpenAPI Specification)๋ผ๊ณ ๋ ๋ถ๋ฅด๋๋ฐ RESTful API๋ฅผ ๊ธฐ๋ฐ์ผ๋ก API Spec์ JSON์ด๋ YAML๋ก ํํํ๋ ๋ฐฉ์์ ์๋ฏธํฉ๋๋ค.
์ฆ, OpenApi๋ Restful API ๋์์ธ์ ๋ํ ์ ์ ํ์ค์ ์๋ฏธํฉ๋๋ค.
โป๏ธ RestDocs๋ฅผ ์์ฑํ๋ฉด Swagger๋ก ๋ณํํด์ฃผ๋๋ก ์๋ํ
RestDocs์ ๊ฒฝ์ฐ ์ ๋ขฐ์ฑ์ด ๋์ง๋ง ๊ฐ๋ ์ฑํ๊ณ API Test ์ ๊ณต ๋ฑ Swagger์ ์์ด ํ์ ์ ์์ฌ์ด์ ์ด ์กด์ฌํ๋ค๊ณ ์๊ฐ์ด ๋ค์์ต๋๋ค.
์ด์ Open API spec์ ์ด์ฉํ์ฌ Swagger๋ก ์๋ ๋ณํํด์ฃผ๋ ๋ฐฉ๋ฒ์ ์ ์ฉํด ์ฝ๋ ์์ฑ์ RestDocs๋ก ํ๊ณ ๋ณด์ฌ์ฃผ๋ ๊ฒ์ Swagger๋ก ํ๊ฒ๋ ๊ตฌํํ์์ต๋๋ค.
์ด์ ์ด ๊ณผ์ ์ ์ค์ ๋ก ์ ์ฉํด๋ด ์๋ค.
๋จผ์ ์ ์ฉํ ํ๊ฒฝ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๐ ์ธ์ด : Java 17 ํ๋ ์์ํฌ : Spring boot 2.7.1 ๋น๋ ํด : Gradle
Gradle์ API ๋ฌธ์ํ์ ์๋ ๋ณํ์ ํ์ํ ๋ค์ ์์กด์ฑ์ ์ถ๊ฐํด์ฃผ๋๋ก ํฉ๋๋ค.
// build.gradle
plugins {
...
...
id 'com.epages.restdocs-api-spec' version '0.16.0'
}
dependencies {
...
...
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.16.2'
}
Restdocs๋ก ๋ณํํ OpenAPI์ ์ ๋ณด๋ฅผ Gradle์ ์ถ๊ฐํฉ๋๋ค.
// build.gradle
openapi3 {
server = 'http://localhost:8080' // ์์ ์๋ฒ์ URL ์์ฑ
title = 'RestDocs to Swagger ๋ณํ ํ
์คํธ' // Swagger ์๋์ ํ์ด์ง์ ๋์ค๋ ์ ๋ชฉ
description = 'Restdocs๋ก API ๋ฌธ์ ์์ฑ ํ ์ด๋ฅผ Swagger๋ก ๋ณํํ๋ ํ์ด์ง' // Swagger ํ์ด์ง ์ ๋ชฉ ๋ฐ์ ์ค๋ช
๋์ ์ถ๊ฐ๋๋ ๋ฉ์ธ์ง
version = '0.0.1-SNAPSHOT' // ์ ํ๋ฆฌ์ผ์ด์
๋ฒ์ ์ ๋ณด
format = 'yaml' // json์ผ๋ก๋ ๊ฐ๋ฅ
}
ํ ์คํธ ์คํ ํ snippet์ผ๋ก ์์ฑ๋ openapi3.yaml ํ์ผ์ static ์์ญ์ ์ฎ๊ธธ๋ ์ฌ์ฉํ๋ ์คํฌ๋ฆฝํธ๋ฅผ ์ถ๊ฐํฉ๋๋ค.
// build.grade
task copyTest {
dependsOn("openapi3")
copy {
from "$buildDir/api-spec/openapi3.yaml"
into "src/main/resources/static/docs/."
}
}
์ด์ ๋ณํ๋ OpenAPI๋ฅผ Swagger UI๋ก ๋ณด์ฌ์ฃผ๊ธฐ ์ํด Swagger-UI๋ฅผ ๋ค์ด๋ฐ์ต๋๋ค.
https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/
๋ค์ด ๋ฐ์ Swagger-ui์์ /dist ํด๋ ํ์์ ์๋ ํ์ผ๋ค์ src/main/resources/static/docs ๊ฒฝ๋ก๋ก ์ด๋์ํต๋๋ค.
๋ค์ด ๋ฐ์ ํ์ผ์ค์ ํ์ ์๋ ํ์ผ์ธ ๋ค์ ํ์ผ์ ์ญ์ ํฉ๋๋ค.
- oauth2-redirect.html
- swagger-ui.js
- swagger-ui-es-bundle-core.js
- swagger-ui-es-bundle.js
์ญ์ ๋ฅผ ์๋ฃํ์ผ๋ฉด index.html์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํฉ๋๋ค.
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/static/docs/swagger-ui.css" />
<link rel="icon" type="image/png" href="/static/docs/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/static/docs/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="/static/docs/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="/static/docs/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/static/docs/openapi3.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
</html>
์ด์ ๊ฐ๋จํ๊ฒ ํ ์คํธ ์ฉ๋๋ก ์ปจํธ๋กค๋ฌ๋ฅผ ํ๋ ๋ง๋ค์ด๋ด ์๋ค.
@RestController
@RequestMapping("/user")
public class MainController {
@GetMapping
public ResponseEntity<MainResponse.Get> get() {
return ResponseEntity.ok(
new MainResponse.Get("get test success")
);
}
}
ํด๋น ์ปจํธ๋กค๋ฌ์ ๋ํด์ ๋ค์๊ณผ ๊ฐ์ ํ ์คํธ๋ฅผ ์ถ๊ฐํด์ค์๋ค.
@WebMvcTest(MainController.class)
@AutoConfigureMockMvc
@AutoConfigureRestDocs
class MainControllerTest {
@Autowired
private MockMvc mockMvc;
private final ObjectMapper mapper = new ObjectMapper();
@Test
@DisplayName("Get ํ
์คํธ")
void getTest() throws Exception {
mockMvc.perform(
RestDocumentationRequestBuilders.get("/user")
)
.andExpect(status().isOk())
.andDo(MockMvcRestDocumentationWrapper.document("test-get",
ResourceSnippetParameters.builder()
.tag("ํ
์คํธ")
.summary("Get ํ
์คํธ")
.description("Get ํ
์คํธ")
.responseSchema(Schema.schema("MainResponse.Get"))
,
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
responseFields(
fieldWithPath("message").type(JsonFieldType.STRING).description("๋ฉ์ธ์ง")
)
));
}
}
ํ์ฌ static์ ๋ฃ์ด๋ ์ค์จ๊ฑฐ UI์ ์ ๊ทผํ ์ ์๋๋ก ๋ค์ ์ค์ ์ ์ถ๊ฐํฉ๋๋ค.
@Configuration
public class StaticRoutingConfigure implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
}
}โ
์์ฑ๋ ํ
์คํธ๋ฅผ ์คํํ ํ build.gradle์์ ์์ฑํ copyTest๋ฅผ ์คํ์ํต๋๋ค. ๋ง์ฝ src/main/resources/static/docs/openapi3.yaml์ด ์์ฑ๋ฌ๋ค๋ฉด ์ ๋์๋ ๊ฒ์
๋๋ค.
์ด์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์คํํ๊ณ http://{domainname}/docs/index.html๋ก ์ด๋ํ์ ๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ฉด์ด ๋์จ๋ค๋ฉด ์ฑ๊ณต์ ์ผ๋ก ์คํ๋ ๊ฒ์ ๋๋ค.
๐ง๐ป Swagger ํ๋ฉด ์ปค์คํ ํ๊ธฐ
OpenApi๋ฅผ ์ฌ์ฉํ์ฌ RestDocs๋ก ์ง ํ ์คํธ ์ฝ๋๋ฅผ Swagger๋ก ์๋๋ณํ ํด์ฃผ๋ ๊ฒ์ ๊ฐํธํ์ง๋ง Restdocs ๋ฌธ๋ฒ์ผ๋ก๋ง OpenApi Spec์ ์ด์ฉํ์ฌ Swagger ์๋๋ณํ์ ํ๋ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ํด๋น API์ ๋ํ ์ค๋ช ์ด ๋น์ฝํ ์ ์์ต๋๋ค.
์ฌ๊ธฐ์ ์คํค๋ง์ ๊ฒฝ์ฐ Request์ Response์ ๋ํ ๊ฐ์ฒด ๋ด์ฉ์ด ๋ค์ด๊ฐ๊ฒ ๋๋๋ฐ ์์ ๊ฐ์ด ํํ๋๋ฉด ์ด๊ฒ ์ด๋ค ์ญํ ์ ํ๋ ๊ฒ์ธ์ง ์๊ธฐ ํ๋ค ์ ์์ต๋๋ค.
ํ ์คํธ ์ฝ๋ ์์ฑ์ ํ ๋ ResourceSnippetParameters ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด์ ๋ํ ์ ๋ณด๋ฅผ ์ปค์คํ ํ ์ ์์ต๋๋ค.
๋จผ์ RestDocs๋ก๋ง ์์ฑํ ์ฝ๋๋ฅผ ํ์ธํด๋ด ์๋ค.
@Test
@DisplayName("Post ํ
์คํธ")
void postTest() throws Exception {
MainRequest.Post request = new Post("post request");
mockMvc.perform(
RestDocumentationRequestBuilders.post("/user")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request))
)
.andExpect(status().isCreated())
.andDo(MockMvcRestDocumentationWrapper.document("test-post",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("message").type(JsonFieldType.STRING).description("์์ฒญ ๋ฉ์์ง")
),
responseFields(
fieldWithPath("id").type(JsonFieldType.NUMBER).description("์์ฑ ID")
)));
}
์ผ๋ฐ์ ์ธ Restdocs์ ๋ค๋ฅธ์ ์ MockMvcRestDocumentationWrapper ๋ฅผ ์ฌ์ฉํ์ฌ API ๋ฌธ์ํ๋ฅผ ์ํจ๋ค๋ ์ ์ ๋๋ค.
๋ง์ฝ, ์ด์๋ํด์ ์ปค์คํ ํ๊ณ ์ถ์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ด ResourceSnippetParameters ๋ฅผ ์ถ๊ฐํ์ฌ ์ฌ๋ฌ ์ ๋ณด๋ฅผ ์์ ํ ์ ์์ต๋๋ค.
...
...
.andExpect(status().isCreated())
.andDo(MockMvcRestDocumentationWrapper.document("test-post",
ResourceSnippetParameters.builder()
.tag("ํ
์คํธ")
.summary("Post ํ
์คํธ")
.description("Post ํ
์คํธ")
.requestSchema(Schema.schema("MainRequest.Post"))
.responseSchema(Schema.schema("MainResponse.Post"))
,
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
...
...
- ResourceSnippetParameters.builder()
์ง์ ์์ฑํ API ์ ๋ณด๋ฅผ ์ถ๊ฐํ ๋ ์ฌ์ฉ๋ฉ๋๋ค. - tag(String)
์์ ์ด๋ฏธ์ง์์ “ํ ์คํธ"์ ํด๋นํ๋ ๋ถ๋ถ์ผ๋ก ํ๊ทธ๋ฅผ ํตํด ์ฌ๋ฌAPI๋ฅผ ๋ฌถ์ ์ ์์ต๋๋ค. - summary(String)
API์ ์์ฒญ URL ์์ ๋ค์ด๊ฐ๋ ์ ๋ชฉ์ ๋๋ค. - description(String)
API ์ธ๋ถ ์ ๋ณด๋ก ๋ค์ด๊ฐ์ ๋ ์กด์ฌํ๋ ์ค๋ช ์ ๋๋ค. - requestSchema(Schema)
ํด๋น API๋ฅผ ํธ์ถํ ๋ ์ฌ์ฉํ ๋ณธ๋ฌธ๊ณผ ๋งคํ๋๋ ๊ฐ์ฒด์ ์ด๋ฆ์ ๋๋ค. - responseSchema(Schema)
ํด๋น API์ ๋ฐํ ๊ฐ์ ๋ณธ๋ฌธ๊ณผ ๋งคํ๋๋ ๊ฐ์ฒด์ ์ด๋ฆ์ ๋๋ค.
๐ ์์ ์ฝ๋
https://github.com/zxcv9203/openapi_spec_restdocs_example.git
๐ง ์ฐธ๊ณ ์๋ฃ
https://gruuuuu.github.io/programming/openapi/
https://velog.io/@bread_dd/Spring-Rest-Docs์-Open-Api-Swagger
https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/
https://github.com/ePages-de/restdocs-api-spec
https://github.com/traeper/api_documentation
'Java > Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
์คํ๋ง์์ ์ด๋ป๊ฒ ์บ์ฑ์ด ์ด๋ฃจ์ด์ง๊น? - 1ํธ ์คํ๋ง์์ ์บ์ฑ ์ฌ์ฉํ๊ธฐ (1) | 2024.08.04 |
---|---|
์คํ๋ง ๋ถํธ 3.0 ์ด์์์ QueryDSL ์ค์ (2) | 2022.12.03 |
@RequestBody๋ก ์ง์ ํ DTO์ ๊ธฐ๋ณธ์์ฑ์๊ฐ ํ์ํ ์ด์ (2) | 2022.10.11 |
์คํ๋ง ๋น๊ณผ ์คํ๋ง ์ปจํ ์ด๋ (0) | 2022.09.04 |
IoC์ DI, ๊ทธ๋ฆฌ๊ณ ์์กด์ฑ ์ฃผ์ ์ ์ํ ๋ฐฉ๋ฒ๋ค (2) | 2022.08.29 |