🤨 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에 의존성 추가
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' }
- Open API 정보 추가하기
// 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/." } }
- 이제 Swagger를 적용하기 위해 Swagger-ui를 다운받습니다.
다운로드 링크 : https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/
해당 링크에서 Plain old HTML/CSS/JS (Standalone)의 latest releast를 선택하여 다운할 수 있습니다.

선택하면 다음과 같은 깃허브 페이지로 이동할 수 있습니다.

- 다운 받은 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 resource에 접근하기 위해 다음과 같은 설정을 스프링에 추가합니다.
@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
로 이동했을 때 다음과 같은 화면이 나온다면 성공적으로 실행된 것입니다.

위의 코드만 작성했다면 Get 밖에 없는 것이 정상입니다.
🧑💻 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의 반환 값의 본문과 매핑되는 객체의 이름입니다.