
API 문서를 작성할 때에는 크게 2가지 방법이 있다. 바로 Swagger UI 와 Spring Rest Docs 이다.
| Swagger UI | Spring REST Docs |
|---|---|
| - Api Controller, Dto 부분에 애노테이션을 추가하여 문서가 작성되는 방식 | 테스트 코드로 문서를 작성하는 방식 (테스트가 성공해야만 작성할 수 있음) |
| API 테스트가 가능하다. | API 테스트가 불가능하다. |
위의 표처럼 Swagger UI 는 문서도 깔끔하고, API 테스트를 바로 할 수 있지만, 문서를 위한 어노테이션을 Controller, Dto 부분에 작성해야하기 때문에 가독성이 떨어진다.
따라서 이번 Tortee 프로젝트에는 Spring Rest Docs 를 선택하였다.
Spring REST Docs 는 RESTful 서비스 문서화에 사용된다. 각 테스트에서 스니펫이라고 하는 문서 조각을 만들어줘 이를 하나의 문서로 꾸밀 수 있다.
문서는 adoc 라는 확장자를 사용하며, AsciiDoc 문법을 따른다. 이는 반드시 테스트를 통과해야 문서가 생성되기 때문에 서비스 자체의 신뢰도가 높아진다.
Spring Rest Docs 로 테스트 코드를 작성할 때, 대표적으로 MockMvc 와 Rest Assured 를 사용한다.
@WebMvcTest 로 테스트할 수 있다.@WebMvcTest 는 Application Context를 완전하게 Start하지 않고, PresentationLayer 를 스캔하여 빈등록한다.@SpringBootTest 로 테스트를 할 수 있다.@WebMvcTest 에도 적합하다.service 계층에 대한 테스트는 tortee-domain 모듈에서 JUnit5 로 진행 예정이다.plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
id 'org.asciidoctor.jvm.convert' version '3.3.2' // 1
id 'java'
}
group 'com.semoib'
version '1.0-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
configurations {
asciidoctorExtensions
}
// 2
ext {
// 스니펫 생성 위치 변수에 저장
snippetsDir = file("${buildDir}/generated-snippets")
originHtml = file("${buildDir}/docs/asciidoc/index.html")
targetDir = file("src/test/resources/static/docs")
}
dependencies {
implementation project(":tortee-domain")
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
/* Rest Docs */
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // 5
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor' // 6
}
test {
outputs.dir snippetsDir // 3
useJUnitPlatform()
finalizedBy 'asciidoctor'
}
asciidoctor { // 4
dependsOn test
configurations 'asciidoctorExtensions'
inputs.dir snippetsDir
}
task copyAsciidoctor(type: Copy) { // 7
dependsOn asciidoctor
from originHtml
into targetDir
}
bootRun {
dependsOn copyAsciidoctor // 8
}
build/generated-snippets 로 지정한다.snippetsDir 로 지정한다.targetDir 로 복사한다.이제 Spring Rest Docs 를 통해 문서를 생성하기 위해 간단한 테스트코드를 구현해보자.
@RestController
public class HealthCheckController {
@GetMapping("/health-check")
public String healthCheck() {
return "Status is ok";
}
}
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) // 1
@AutoConfigureRestDocs
@WebMvcTest(controllers = HealthCheckController.class)
class HealthCheckControllerTest {
private MockMvc mockMvc;
@BeforeEach // 2
public void setUp(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
@DisplayName("Health Check Controller Test")
@Test
public void healthCheckTest() throws Exception {
// given
// when
ResultActions result = mockMvc.perform(get("/api/health-check")
.contentType(MediaType.APPLICATION_JSON));
// then
result.andExpect(status().isOk()) // 3
.andExpect(content().string("Status is ok"))
.andDo(print())
.andDo(document("{class-name}/{method-name}",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint())
))
;
}
}

= Service Api Rest Docs
tortee;
이 문서는 Service-Api 에 대한 문서입니다.
:doctype: book
:icons: font
:source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용
:toc: left // toc (Table Of Contents)를 문서의 좌측에 두기
:toclevels: 2
:sectlinks:
[[Health-Check-API]]
== Health Check API
---
Health Check Api 정리
cURL:
include::{snippets}/health-check-controller-test/health-check-test/curl-request.adoc[]
HTTP request:
include::{snippets}/health-check-controller-test/health-check-test/http-request.adoc[]
HTTP response:
include::{snippets}/health-check-controller-test/health-check-test/http-response.adoc[]
include::{snippets}/ 를 통해 우리는 snippets 문서 조각을 커스텀해서 문서 양식을 지정할 수 있다.우리는 위의 과정을 거쳐 Spring Rest Docs 를 통해 API 문서를 만들 수는 있었으나, 테스트 코드에 문서 작성을 위한 기본 설정들이 많이 들어가 있는 부분을 알 수 있다.
또한 Service-Api 모듈에서는 WebMvcTest 로 진행할 것이기 때문에, 각 ControllerTest 부분에서 WebMockMvc 와 Rest Docs 를 공통 적용하는 부분을 다른 객체에서 관리할 수 있는 부분이 있다.
andDo(document()) 부분에서 문서명을 항상 지정해주는 부분prettyPrint() , andDo(print()) 부분이 중복해서 선언해줘야 하는 부분asciidoctor {
dependsOn test
configurations 'asciidoctorExtensions'
inputs.dir snippetsDir
finalizedBy 'copyAsciidoctor' // 2
}
asciidoctor.doFirst { // 1
delete targetDir
}
먼저 targetDir 에 생성되었던 api 문서를 gradlew build 만으로도 자동으로 삭제 및 생성할 수 있도록 설정을 추가해주었다.
targetDir 삭제 copyAsciidoctor 자동 실행
ControllerTest 는 abstract class 로 , 모든 테스트 간 공통으로 사용되는 부분을 정의하는 용도로 생성되었습니다.
@Disabled
public abstract class ControllerTest {
protected MockMvc mockMvc;
protected ObjectMapper objectMapper;
protected String createJson(Object dto) throws JsonProcessingException {
return objectMapper.writeValueAsString(dto);
}
}
RestDocsConfig 는 Document 에 공통적으로 적용되는 설정들을 선언하였습니다.
@TestConfiguration
public class RestDocsConfig {
@Bean
public RestDocumentationResultHandler write(){ // 1
return MockMvcRestDocumentation.document(
"{class-name}/{method-name}",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint())
);
}
public static final Attributes.Attribute field(
final String key,
final String value){
return new Attributes.Attribute(key,value);
}
}
RestDocsTestSupport 는 RestDocsConfig 에 적용된 Document 설정들을 ControllerTest 에서 선언된 mockMvc 객체에 주입하기 위한 클래스입니다.
@Disabled
@Import(RestDocsConfig.class) // 2
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) // 1
public class RestDocsTestSupport extends ControllerTest {
@Autowired
protected RestDocumentationResultHandler restDocs;
@BeforeEach
void setUp(final WebApplicationContext context,
final RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.alwaysDo(MockMvcResultHandlers.print())
.alwaysDo(restDocs)
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.build();
}
}
@AutoConfigureRestDocs 를 통해 자동으로 주입하였으나, 이제는 커스텀해서 사용하기 때문에 해당 어노테이션을 제거합니다.이제 Document에 적용되던 중복 부분을 제거된 테스트 코드입니다.
@WebMvcTest(HealthCheckController.class)
class HealthCheckControllerTest extends RestDocsTestSupport {
@DisplayName("Service Api 헬스체크_테스트")
@Test
public void healthCheckTest() throws Exception {
// given
// when
ResultActions result = mockMvc.perform(get("/api/health-check")
.contentType(MediaType.APPLICATION_JSON));
// then
result.andExpect(status().isOk())
.andExpect(content().string("Status is ok"))
;
}
}
리팩토링 전과 마찬가지로 Spring Rest Docs 문서가 정상적으로 생성되는 것을 알 수 있습니다.