개발한 중고거래장터 wiki 페이지에 등록할 API 문서를 작성하기 위해 Spring Rest Docs를 사용하기로 결정했습니다.
Swagger도 많이 사용하지만, Swagger는 API 테스트 작업에 특화되어 있습니다. 테스트가 아닌 문서 작성 기능만 필요하기 때문에 Swagger와 비교해서 좀 더 깔끔한 문서를 만들 수 있는 Spring Rest Docs를 선택했습니다.
Spring Rest Docs 적용에 다음과 같은 기술을 사용했습니다.
- Spring 5
- Maven
- JUnit4
- MockMVC
- AsciiDoc
그리고 개발 환경은 다음과 같습니다.
- Spring Legacy Project(MVC)
- Spring 5.0.7
- Maven
- Java 1.8
- MySQL 8.0.27
- Redis-x64-3.2.100
구글링을 통해 수많은 Spring Rest Docs 적용 정보를 얻을 수 있습니다만, Spring Boot와 Gradle 환경에서 적용할 때 필요한 정보가 대부분입니다.
저는 Spring Legacy Project(MVC) 그리고 Maven 환경에서 개발했기 때문에 해당 환경에 맞게 적용 했음을 참고하시면 됩니다.
먼저 restdocs 의존성과 plugin 설정을 추가합니다.
pom.xml
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>2.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
pom.xml
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>2.0.2.RELEASE</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.outputDirectory}/static/docs</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}/generated-docs </directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
spring-restdocs-asciidoctor
- .adoc(ascii 파일의 확장자) 파일이 target/generated-snippets 폴더를 바라보도록 설정합니다. JUnit 테스트를 수행하면 target/generated-snippets 경로에 테스트한 메소드 폴더가 생성됩니다. 폴더 내부에는 .adoc 형태로 테스트 결과에 대한 내용이 저장되어 있습니다.
${project.build.outputDirectory}/static/docs
- Maven 빌드 시 /static/docs 경로에 사용자가 작성한 .adoc 파일에 대한 html 파일을 생성하도록 설정합니다.
${project.build.directory}/generated-docs
- 테스트 결과 정보를 담은 snippet이 생성되는 경로를 설정합니다.
참고로 Maven의 기본 경로는 다음과 같습니다.
- 빌드 : ${project.build.directory} = ${pom.build.directory} = ‘target’
- 빌드 : ${project.build.outputDirectory} = ‘target/classes’
- 프로젝트 이름 : ${project.name} = ${pom.name} = / 엘리먼트 설정 값
- 프로젝트 버전 : ${project.version} = ${pom.version} = ${version} = / 엘리먼트 설정 값
- 최종 파일 이름 : ${project.build.finalName}= // 엘리먼트 설정 값
JUnit 테스트 코드를 작성합니다.
UserControllerTest.java
package com.board.controller;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.requestParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import javax.servlet.ServletException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import com.board.domain.UserVO;
import com.google.gson.Gson;
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml", "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
@Transactional
public class UserControllerTest {
private final static String TEST_VALUE = "testVal";
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Autowired
private UserController userController;
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
private UserVO user;
private RestDocumentationResultHandler document;
@Before
public void setUp() throws ServletException {
this.document = document("{class-name}/{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()));
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(document).build();
}
@Before
public void setUpMockMvc() throws Exception {
this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}
@Before
public void setUpUser() {
user = UserVO.builder()
.accountId(TEST_VALUE)
.userId(TEST_VALUE)
.userPwd(TEST_VALUE)
.userName(TEST_VALUE)
.userEmail("testVal@gmail.com")
.userPhone("01012341234")
.userAddr(TEST_VALUE)
.build();
}
@Test
public void Successfully_create_new_user() throws Exception {
String jsonStr = new Gson().toJson(user);
mockMvc.perform(RestDocumentationRequestBuilders.post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonStr))
.andExpect(status().isOk())
.andDo(document);
}
@Test
public void Can_use_this_email() throws Exception {
mockMvc.perform(RestDocumentationRequestBuilders.get("/users/signup/email").param("userEmail", user.getUserEmail()))
.andExpect(status().isOk())
.andDo(document.document(requestParameters(parameterWithName("userEmail").description("사용자 이메일"))));
}
@Test
public void Can_use_this_id() throws Exception {
mockMvc.perform(RestDocumentationRequestBuilders.get("/users/signup/id").param("userId", user.getUserId()))
.andExpect(status().isOk())
.andDo(document.document(requestParameters(parameterWithName("userId").description("사용자 아이디"))));
}
}
구현 내용 설명
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
- 자동 생성될 문서들의 Root 디렉토리를 지정합니다.
- 기본값은 Maven은 target/generated-snippets, Gradle은 build/generated-snippets 입니다.
- 각 문서들은 위에 설정된 Root 디렉토리의 하위 디렉토리에 자동으로 생성됩니다.
this.document = document("{class-name}/{method-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()));
- RestDocumentationResultHandler 를 {class-name}/{method-name} 로 설정하면 해당 테스트 클래스의 이름과 메소드 이름을 기본으로 디렉토리 경로가 설정되어 snippets을 생성합니다.
- prettyPrint() 메소드를 사용하면 결과물이 이쁘게 출력된다고 합니다.
필요에 따라 pathParameter 또는 requestParameters 등을 사용해서 테스트를 꼼꼼하게 작성하면 됩니다.
테스트 성공 후 Maven Update 작업을 하면 target 경로에 아래와 같은 결과물이 생성됩니다.
http-reuqest/response, request-body/parameter 등의 다양한 테스트 결과물을 확인할 수 있습니다. 이제 이 결과물을 한 곳에 모아서 한 눈에 볼 수 있는 API 문서를 작성해야 합니다.
문서화 작업
생성된 .adoc 파일을 한 곳에 모아서 하나의 html 페이지로 만들어야 가독성 좋은 문서가 됩니다.
src/main/asciidoc 경로에 .adoc 파일을 생성합니다. 파일 이름은 자유롭게 작성할 수 있습니다.
굳이 src/main/asciidoc 경로에 만들어야 .adoc 파일을 인식해서 html 파일로 변환시킬 수 있습니다.
경로는 Maven, Gradle에 따라 다릅니다.
Build tool | Source files | Generated files |
Maven | src/main/asciidoc/*.adoc | target/generated-docs/*.html |
Gradle | src/docs/asciidoc/*.adoc | build/asciidoc/html5/*.html |
.adoc 파일 작성에 필요한 문법은 링크를 참고합니다.
- {snippets}는 target/generated-snippets 경로를 의미합니다.
- html 문서에 노출되기를 원하는 결과물을 include 하면 됩니다.
api-doc.adoc
= 회원가입 [POST]
include::{snippets}/user-controller-test/successfully_create_new_user/curl-request.adoc[]
=== 요청 구조
include::{snippets}/user-controller-test/successfully_create_new_user/http-request.adoc[]
=== 응답 구조
include::{snippets}/user-controller-test/successfully_create_new_user/http-response.adoc[]
= 이메일 중복 확인 [GET]
include::{snippets}/user-controller-test/can_use_this_email/curl-request.adoc[]
=== 요청 구조
include::{snippets}/user-controller-test/can_use_this_email/http-request.adoc[]
==== 요청 파라미터들
include::{snippets}/user-controller-test/can_use_this_email/request-parameters.adoc[]
=== 응답 구조
include::{snippets}/user-controller-test/can_use_this_email/http-response.adoc[]
= 아이디 중복 확인 [GET]
include::{snippets}/user-controller-test/can_use_this_id/curl-request.adoc[]
=== 요청 구조
include::{snippets}/user-controller-test/can_use_this_id/http-request.adoc[]
==== 요청 파라미터들
include::{snippets}/user-controller-test/can_use_this_id/request-parameters.adoc[]
=== 응답 구조
include::{snippets}/user-controller-test/can_use_this_id/http-response.adoc[]
Maven 빌드
Maven 빌드 작업을 합니다.
pom.xml에서 ${project.build.outputDirectory}/static/docs 설정한 대로 해당 경로에 .html 파일이 생성되었습니다.
정적 리소스 접근 경로 설정(Optional)
생성된 .html 파일을 보려면 [경로]/docs/[파일 이름].html 로 접근하면 됩니다. 만약 404 에러가 발생한다면 아래 내용을 참고합니다.
/static 경로의 파일은 브라우저 URL 호출을 통해 바로 접근할 수 있습니다. 그 전에 정적 리소스 접근 경로 설정이 필요합니다.
XML 설정을 사용한다고 가정하고 다음과 같은 정적 리소스 접근 경로를 추가합니다.
<resources mapping="/docs/**" location="/WEB-INF/classes/static/docs/"/>
자동 목차 생성
아래 이미지와 같이 문서 내용에 대한 목차를 생성하면 가독성이 높아진다.
목차는 =(h1), ==(h2) 기호를 기준으로 생성합니다.
:toc: 를 입력하면 목차를 생성합니다. 그리고 left 입력하면 왼쪽에 목차를 생성합니다.
:toclevels: 는 목차에 표현될 = 의 단계를 지정합니다.
주의할 점이 하나 있습니다. 맨 위의 = 기호 바로 밑에 :toc: 를 입력해야 목차가 생성됩니다. 한 칸이라도 개행이 발생하거나 다른 문자가 사이에 있다면 목차를 생성하지 않습니다.
후기
Spring Rest Docs 사용은 규모가 큰 프로젝트에 사용하면 좋겠다고 생각했습니다. 그만큼 테스트가 많고, 한 눈에 구현 정보를 파악해야 할 필요가 있으니까요. 세부 구현 내용을 꼼꼼하게 숙지해서 이후 진행될 프로젝트에도 문제없이 사용할 수 있도록 해야겠습니다.
참고
'Programming > 개인 프로젝트' 카테고리의 다른 글
[중고거래장터 - Study&Refactoring] locale 변경 (0) | 2022.04.07 |
---|---|
[중고거래장터] Server Architecture (0) | 2022.04.03 |
[중고거래장터 - Study&Refactoring] 캐싱 기능 적용(Spring MVC + Redis + Jedis) Java 클래스 방식 (0) | 2022.03.31 |
[중고거래장터 - Study&Refactoring] Oracle ~> MySQL로 이동 (0) | 2022.03.22 |
[중고거래장터 - Study&Refactoring] 캐싱 기능 적용(Spring MVC + Redis + Jedis) XML 방식 (0) | 2022.03.15 |