Spring Rest Docs로 REST API 문서 자동화
kt cloud Container 개발팀에서 추구하는 REST API 문서 자동화에 대한 내용을 소개합니다.
Intro
REST API를 구축하면서 가장 중요한 작업 중 하나는 API 문서를 작성하는 것입니다. 잘 문서화된 API는 사용자와 개발자에게 큰 도움이 됩니다. 그러나 API 문서를 수동으로 작성하는 것은 번거롭고 시간이 많이 소요됩니다.
기존에 API 문서는 별도의 word로 작성이 되었었는데, 각 개발자별로 가지고 있는 버전의 차이가 있고 개발과 문서와의 정합성의 차이가 있는 일들이 빈번했습니다. API 문서 관리는 모든 개발자들에게 숙제이며, 어떤 형태로든 쉽고 간단하게 해결할 방법들을 찾아야 합니다.
이 문제를 해결하기 위해 Spring Rest Docs, Swagger를 사용할 수 있습니다.
Spring Rest Docs, Swagger는 다음과 같은 장단점이 존재합니다.
REST API 개발에서 중요한 과제 중 하나는 신뢰성 있는 테스트 코드 작성과 API 문서의 정합성을 유지하는 것입니다. 특히, 복잡한 시스템에서는 시간이 지남에 따라 API 문서가 실제 구현과 불일치하게 될 가능성이 큽니다. 이를 해결하기 위해 Swagger가 아닌 Spring Rest Docs를 사용하면, 테스트와 문서를 연동하여 항상 최신 상태의 API 문서를 유지할 수 있습니다.
이번 포스팅에서는 Spring Rest Docs를 활용하여 테스트 코드의 신뢰성을 유지하고, API 문서와의 정합성을 맞추는 방법을 소개합니다. 이를 통해 실제 개발 환경에서 적용할 수 있는 상품 API 문서화 방식을 보여드리고자 합니다.
Spring Rest Docs란?
Spring Rest Docs는 Spring MVC 기반 애플리케이션을 위한 API 문서화를 자동화하는 라이브러리입니다. 이 라이브러리는 테스트 케이스와 연동되어 API의 입력 및 출력을 문서화하므로, 항상 최신 상태의 문서를 유지할 수 있습니다.
주요 특징:
- 테스트 코드 기반 문서화
- Asciidoctor 형식 사용
- 스니펫(snippets) 생성을 통한 모듈화된 문서 작성
프로젝트 설정
Spring Rest Docs를 사용하기 위해 필요한 몇가지 라이브러리들이 존재합니다. 본 포스팅에서는 최신 개발 트랜드를 이해하고 따라가기 위해 개발 환경은 다음과 같이 최신 버전에서 stable한 버전들로 구성하였습니다.
1. Gradle 의존성 추가
먼저, build.gradle 파일에 Spring Rest Docs와 관련된 의존성을 추가합니다.
// build.gradle groovy plugins { id 'java' id 'org.springframework.boot' version '3.3.4' id 'io.spring.dependency-management' version '1.1.6' id 'org.asciidoctor.jvm.convert' version '3.3.2' // spring rest docs } group = 'com.ktcloud' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } configurations { compileOnly { extendsFrom annotationProcessor } asciidoctorExt // spring rest docs } dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // spring rest docs asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' // spring rest docs } |
2. Asciidoctor 설정
Asciidoctor는 Rest Docs의 결과를 HTML 형식으로 변환하는 데 사용되며, build를 통해 생성된 HTML이 application 정적 path로 copy되도록 설정 합니다.
build.gradle에서 아래 코드를 추가하여 Asciidoctor와 build를 설정합니다.
// build.gradle groovy // spring rest docs ext { snippetsDir = file('build/generated-snippets') } tasks.named('test') { outputs.dir snippetsDir // spring rest docs useJUnitPlatform() } // spring rest docs asciidoctor { inputs.dir snippetsDir dependsOn test configurations 'asciidoctorExt' baseDirFollowsSourceFile() } // spring rest docs tasks.register('copyDocument', Copy) { dependsOn asciidoctor from layout.buildDirectory.dir("docs/asciidoc") into layout.projectDirectory.dir("src/main/resources/static/docs") } // spring rest docs tasks.named('build') { dependsOn copyDocument } |
API 테스트 및 문서화
이제 Spring Rest Docs를 사용하여 API를 테스트하고 문서화할 수 있습니다. 간단한 예제를 살펴보겠습니다.
1. Controller 클래스 생성
SampleController를 생성하여 간단하게 3개의 Rest API를 만들었습니다.
여기에서 특별한 점은 DTO영역을 record로 활용하였습니다.
record는 간결한 DTO(Data Transfer Object)나 불변 객체를 정의하는 데 사용되는 새로운 class 유형으로, Java 14에서 처음 도입되어 Java 16부터 정식 기능이 되었습니다. record는 데이터를 저장하고, 자동으로 불변성과 기본적인 메서드를 제공하는 것이 주요 특징입니다.
또한 필드, 생성자, getter, equals(), hashCode(), toString()를 자동으로 생성해주기 때문에 간결하고 명확한 코딩을 할 수 있습니다.
// SampleController.java @RestController @RequestMapping("/api/sample") public class SampleController { public record SampleRequest(String name, int value) {}; public record SampleResponse(int id, String data, int value) {}; @GetMapping("") public ResponseEntity<SampleResponse> getSample() { SampleResponse response = new SampleResponse(1, "Sample Name", 0); return ResponseEntity.ok(response); } @GetMapping("/{id}") public ResponseEntity<SampleResponse> getSample(@PathVariable("id") int id) { SampleResponse response = new SampleResponse(id, "Sample " + id, 0); return ResponseEntity.ok(response); } @PostMapping public ResponseEntity<SampleResponse> createSample(@RequestBody SampleRequest request) { SampleResponse response = new SampleResponse(123, request.name, request.value); return ResponseEntity.status(HttpStatus.CREATED).body(response); } } |
2. 테스트 클래스 작성
다음으로, 위의 API를 테스트하고 Spring Rest Docs를 사용하여 문서를 생성할 SampleControllerTest를 작성합니다.
REST API의 기능 검증에 효과적인 테스트 모듈인 MockMvc를 활용하였습니다. 앞에 생성한 SampleController의 테스트 코드이며, 일반적인 JUnit과MockMvc 사용 측면에서 차이점은 @BeforeEach 어노테이션에 API 문서 자동화를 위한 메소드를 생성하고, 각 테스트 케이스별로 andDo(document~ 함수를 사용하여 스니펫(snippets)을 생성한다는 점입니다.
// SampleControllerTest.java @AutoConfigureRestDocs(outputDir = "target/snippets") @WebMvcTest(SampleController.class) @ExtendWith(RestDocumentationExtension.class) public class SampleControllerTest { @Autowired private MockMvc mockMvc; @BeforeEach void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .apply(documentationConfiguration(restDocumentation)) .build(); } @Test void getSampleEndpoint() throws Exception { mockMvc.perform(get("/api/sample") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").exists()) .andDo(print()) .andDo(document("sample-get", responseFields( fieldWithPath("id").description("The sample ID"), fieldWithPath("data").description("The sample data"), fieldWithPath("value").description("The sample value") ) )); } @Test void getSampleEndpointWithPathVariable() throws Exception { mockMvc.perform(get("/api/sample/{id}", 1) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo(print()) .andDo(document("sample-get-with-path-variable", pathParameters( parameterWithName("id").description("The sample ID") ), responseFields( fieldWithPath("id").description("The sample ID"), fieldWithPath("data").description("The sample data"), fieldWithPath("value").description("The sample value") ) )); } @Test void postSampleEndpoint() throws Exception { String requestBody = "{\"name\":\"Test Sample\",\"value\":123}"; mockMvc.perform(post("/api/sample") .contentType(MediaType.APPLICATION_JSON) .content(requestBody) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()) .andDo(print()) .andDo(document("sample-post", requestFields( fieldWithPath("name").description("The name of the sample"), fieldWithPath("value").description("The value of the sample") ), responseFields( fieldWithPath("id").description("The generated sample ID"), fieldWithPath("data").description("The data of the created sample"), fieldWithPath("value").description("The value of the created sample") ) )); } } |
3. API 문서 포맷 만들기
프로젝트의 src/docs/ascidoc위치에 index.adoc 파일을 생성하고 API 문서 포맷을 작성합니다.
Junit으로 Test 실행하여 생성된 snippets 파일들을 index.adoc에 포함시켜서 API 문서 자동화를 실행하면 테스트 조각들이 통합되어 함께 생성됩니다.
= Sample API Documentation :doctype: book :icons: font :source-highlighter: highlightjs :toc: left :toclevels: 3 :sectlinks: == Introduction KT Cloud Container Develoger gil-seong.jeong. This document describes the Sample API endpoints. == API Endpoints === Get Sample This endpoint retrieves a sample. ==== Request include::{snippets}/sample-get/http-request.adoc[] ==== Response include::{snippets}/sample-get/http-response.adoc[] ==== Response Fields include::{snippets}/sample-get/response-fields.adoc[] === Get Sample with ID This endpoint retrieves a sample by its ID. ==== Request include::{snippets}/sample-get-with-path-variable/http-request.adoc[] ==== Path Parameters include::{snippets}/sample-get-with-path-variable/path-parameters.adoc[] ==== Response include::{snippets}/sample-get-with-path-variable/http-response.adoc[] ==== Response Fields include::{snippets}/sample-get-with-path-variable/response-fields.adoc[] === Create Sample This endpoint creates a new sample. ==== Request include::{snippets}/sample-post/http-request.adoc[] ==== Request Fields include::{snippets}/sample-post/request-fields.adoc[] ==== Response include::{snippets}/sample-post/http-response.adoc[] ==== Response Fields include::{snippets}/sample-post/response-fields.adoc[] |
4. API 문서 자동화 생성
사용중인 IDE에서 Terminal을 열고 아래와 같은 명령어를 실행합니다.
Junit으로 작성된 Test 코드들을 실행되고 build/docs/asciidoc/ 위치에 index.html이 생성됩니다.
./gradlew asciidocto BUILD SUCCESSFUL in 1m 16s 5 actionable tasks: 2 executed, 3 up-to-date |
다시 Terminal에서 아래와 같은 명령어를 실행하여 gradle build를 진행합니다. build가 완료되면 build/docs/asciidoc/index.html 파일이 application의 정적 디렉토리 위치에서 load 될 수 있도록 src/main/resources/static/docs/ 로 copy 됩니다.
./gradlew build BUILD SUCCESSFUL in 4s 9 actionable tasks: 3 executed, 6 up-to-date |
5. API 문서확인
index.html을 실행시키면 브라우저를 통해 API 문서를 확인 할 수 있지만, Spring project를 구동하여 index.html을 load하여 API 문서를 확인할 수도 있습니다. 이 방법을 통하여 현재 상용중인 서비스의 신뢰성있는 openAPI를 제공할 수 있습니다.
실행시킨 application의 docs/index.html 경로를 보면 web api 문서로 정의된 내용을 확인 할 수 있으며, 앞전에 MockMVC와 JUnit Test 코드 및 결과 내용까지 모두 확인할 수 있습니다.
해당 문서를 PDF로 변환 할때도 브라우저의 PDF 출력 기능을 사용하면
아래와 같이 깔끔하게 페이징 처리되어 오프라인으로도 API 문서를 확인 할 수 있습니다.
마무리
Spring Rest Docs를 활용하는 방법들은 다양합니다.
위 포스팅한 내용은 그중에 일부이며 방식에는 장단점이 존재합니다.
현재 방식에서는 adoc를 직접 작성하는 부분이 api 문서에 디테일한 내용들로 채우는 장점은 있지만 많은 Rest API 문서를 자동화를 만드는것에 있어서 활용도가 떨어지는 부분도 있습니다. adoc 파일 또한 자동화 생성이 되도록 보완한다면 Spring Rest Docs를 활용하는 가장 최고의 모듈이 될 것 같습니다. 그리고 build 될때 만들어진 스니펫(snippets)을 postman으로 활용하기 위한 json형태로 자동으로 만들어지게 한다면 API를 어디서나 쉽게 테스트하고 활용할 수 있는 좋은 방안이 될 것입니다.
결과적으로, Spring REST Docs를 사용하면 테스트 코드를 통해 정확하고 최신의 API 문서를 자동으로 생성할 수 있습니다. 이는 개발 생산성을 향상시키고 API 문서의 신뢰성을 높이는 데 큰 도움이 됩니다. 프로덕션 코드에 영향을 주지 않으면서도 항상 최신 상태의 문서를 유지할 수 있어, 팀의 협업과 프로젝트 품질 향상에 크게 기여할 수 있습니다.
참고/출처
'Tech story > Container' 카테고리의 다른 글
eBPF 기반의 강력한 쿠버네티스 네트워킹: Cilium CNI 소개 (1) | 2024.10.24 |
---|---|
Kubernetes Control Plane과 친해지기 #1 (1) | 2024.10.24 |
알아보기 1. Container Basic (1) | 2024.10.24 |
Kybernetes 오픈소스 생태계 탐구: #1. Prometheus와 함께하는 Kubernetes 모니터링 (3) | 2024.10.24 |
[개발자 인터뷰] “kt cloud 멀티클라우드 구축 기술 제공으로, K-PaaS 생태계 주도권 선점 할 것” (0) | 2023.08.17 |