개발자의 오르막
Spring Boot Rest Docs 멀티모듈 프로젝트에 적용 본문
Keyword
API , Rest Docs
Overview
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
Spring REST Docs 는 RESTful 서비스 문서화에 사용된다. 각 테스트에서 스니펫이라고 하는 문서 조각을 만들어줘 이를 하나의 문서로 꾸밀 수 있다.
문서는 adoc 라는 확장자를 사용하며, AsciiDoc 문법을 따른다. 이는 반드시 테스트를 통과해야 문서가 생성되기 때문에 서비스 자체의 신뢰도가 높아진다.
Spring Rest Docs 로 테스트 코드를 작성할 때, 대표적으로 MockMvc 와 Rest Assured 를 사용한다.
- MockMvc
- MockMvc 를 사용하면 @WebMvcTest 로 테스트할 수 있다.
- @WebMvcTest 는 Application Context를 완전하게 Start하지 않고, PresentationLayer 를 스캔하여 빈등록한다.
- MockMvc 를 사용하면 @WebMvcTest 로 테스트할 수 있다.
- Rest Assured
- RestAssured 는 @SpringBootTest 로 테스트를 할 수 있다.
- @SpringBootTest 의 경우 모든 빈을 로드하여 등록하기 때문에 테스트 속도가 느리다.
- RestAssured 는 @SpringBootTest 로 테스트를 할 수 있다.
Choice Spec : MockMvc 이유
- tortee 프로젝트는 멀티모듈로 구성된 프로젝트이다.
- tortee-app-service-api 모듈은 프레젠테이션 계층만을 지원한다.
- 따라서 PresentationLayer 만 스캔하여 빈등록할 수 있는 @WebMvcTest 에도 적합하다.
- 해당 service-api 모듈만 Spring Rest Docs 적용할 수 있다.
- service 계층에 대한 테스트는 tortee-domain 모듈에서 JUnit5 로 진행 예정이다.
개발 스펙
- SpringBoot 2.7.2 RELEASE
- Gradle 7.5.1
- JUnit 5
- Spring REST Docs (MockMvc) 2.0.4 RELEASE
- Asciidoctor 3.3.2
build.gradle
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
}
- gradle7부터 사용하는 플러그인으로 asciidoc 파일을 변환하여 build 디렉터리에 복사하는 플러그인이다.
- gradle 에서 사용하는 변수를 정의하는 부분이다.
- snippetsDir : 생성된 스니펫을 저장할 위치를 정의한다.
- originHtml : Spring Rest Docs 를 통해 생성된 문서의 위치이다.
- targetDir : build 디렉터리에 생성된 문서를 이동시키고자 하는 디렉터리이다.
- 테스트 Task의 결과 아웃풋 디렉토리를 build/generated-snippets 로 지정한다.
- asciidoctor Task 가 사용할 디렉토리를 snippetsDir 로 지정한다.
- MockMvc 를 테스트에 사용하기 위한 의존성을 추가한다.
- 일반 텍스트를 처리하고 HTML 파일을 생성하는 의존성을 추가한다.
- asciidoctor Task 로 생성한 build/docs/asciidoc 파일을 targetDir 로 복사한다.
- bootJar 실행 시 copyAsciidoctor 를 먼저 실행하도록 한다.
이제 Spring Rest Docs 를 통해 문서를 생성하기 위해 간단한 테스트코드를 구현해보자.
HealthCheckController
@RestController
public class HealthCheckController {
@GetMapping("/health-check")
public String healthCheck() {
return "Status is ok";
}
}
HealthControllerTest
@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())
))
;
}
}
- Spring Rest Docs 와 Spring Boot 에서의 테스트를 적용하기 위한 어노테이션이다.
- Mock Mvc 객체를 생성하고, Spring Rest Docs 객체를 주입한다.
- ResultAction 을 통해 테스트가 끝나고, Spring Rest Docs 설정하는 document 를 통해 snippets 을 설정한다.
service-api.adoc
= 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 문서 조각을 커스텀해서 문서 양식을 지정할 수 있다.
Refactoring
우리는 위의 과정을 거쳐 Spring Rest Docs 를 통해 API 문서를 만들 수는 있었으나, 테스트 코드에 문서 작성을 위한 기본 설정들이 많이 들어가 있는 부분을 알 수 있다.
또한 Service-Api 모듈에서는 WebMvcTest 로 진행할 것이기 때문에, 각 ControllerTest 부분에서 WebMockMvc 와 Rest Docs 를 공통 적용하는 부분을 다른 객체에서 관리할 수 있는 부분이 있다.
리팩토링 목표
- 테스트 코드에서 andDo(document()) 부분에서 문서명을 항상 지정해주는 부분
- 테스트 코드에서 문서로 보기좋게 설정한 prettyPrint() , andDo(print()) 부분이 중복해서 선언해줘야 하는 부분
build.gradle
asciidoctor {
dependsOn test
configurations 'asciidoctorExtensions'
inputs.dir snippetsDir
finalizedBy 'copyAsciidoctor' // 2
}
asciidoctor.doFirst { // 1
delete targetDir
}
먼저 targetDir 에 생성되었던 api 문서를 gradlew build 만으로도 자동으로 삭제 및 생성할 수 있도록 설정을 추가해주었다.
- asciidoctor 를 실행하기 앞서 targetDir 삭제
- asciidoctor 를 모두 실행하였으면 copyAsciidoctor 자동 실행
Rest Docs Bean 생성
ControllerTest
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
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);
}
}
- 우리는 이 RestDocsConfig 를 통해 문서의 제목과 prettyPrint() 선언을 공통으로 적용시킬 수 있었습니다.
RestDocsTestSupport
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();
}
}
- 리팩토링한 테스트코드에서는 RestDocs Document 를 @AutoConfigureRestDocs 를 통해 자동으로 주입하였으나, 이제는 커스텀해서 사용하기 때문에 해당 어노테이션을 제거합니다.
- RestDocs 의 Document 설정을 추가해주는 코드입니다.
HealthCheckControllerTest
이제 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 문서가 정상적으로 생성되는 것을 알 수 있습니다.
Todo
- RestDocs 를 통해 얻은 정보들을 Json 으로 추출하여 SwaggerUI 와 연동하는 방법도 존재한다.
- 그러면 우리는 일일이 adoc 문서를 꾸밀 필요가 없어지고, UI 에서 즉각 테스트가 가능해진다.
- Reference
Reference
- https://gaemi606.tistory.com/entry/Spring-Boot-REST-Docs-적용하기
- https://subji.github.io/posts/2021/01/06/springrestdocsexample
- https://velog.io/@gudonghee2000/MockMvc를-사용한-Spring-RestDocs
- https://velog.io/@backtony/Spring-REST-Docs-적용-및-최적화-하기
- https://taetaetae.github.io/posts/a-combination-of-swagger-and-spring-restdocs/
'Toy Project > Tortee' 카테고리의 다른 글
SpringBoot 에 Firebase Admin Auth SDK 적용하기 (0) | 2022.12.12 |
---|---|
Layer Architecture 에 따른 멀티모듈 구성하기 (0) | 2022.11.12 |
지속 가능한 개발을 위한 멀티모듈과 SpringBoot (0) | 2022.10.10 |