개발자의 오르막

Spring Boot Rest Docs 멀티모듈 프로젝트에 적용 본문

Toy Project/Tortee

Spring Boot Rest Docs 멀티모듈 프로젝트에 적용

계단 2022. 10. 17. 14:39

Keyword


API , Rest Docs

 

 

https://www.myerp.pl/sage/jak-przyspieszyc-aktualizowanie-systemow-erp-dzieki-api/

 

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 를 스캔하여 빈등록한다.
  • Rest Assured
    • RestAssured 는 @SpringBootTest 로 테스트를 할 수 있다.
      • @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
}
  1. gradle7부터 사용하는 플러그인으로 asciidoc 파일을 변환하여 build 디렉터리에 복사하는 플러그인이다.
  2. gradle 에서 사용하는 변수를 정의하는 부분이다.
    1. snippetsDir : 생성된 스니펫을 저장할 위치를 정의한다.
    2. originHtml : Spring Rest Docs 를 통해 생성된 문서의 위치이다.
    3. targetDir : build 디렉터리에 생성된 문서를 이동시키고자 하는 디렉터리이다.
  3. 테스트 Task의 결과 아웃풋 디렉토리를 build/generated-snippets 로 지정한다.
  4. asciidoctor Task 가 사용할 디렉토리를 snippetsDir 로 지정한다.
  5. MockMvc 를 테스트에 사용하기 위한 의존성을 추가한다.
  6. 일반 텍스트를 처리하고 HTML 파일을 생성하는 의존성을 추가한다.
  7. asciidoctor Task 로 생성한 build/docs/asciidoc 파일을 targetDir 로 복사한다.
  8. 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())
                ))
        ;
    }
}
  1. Spring Rest Docs 와 Spring Boot 에서의 테스트를 적용하기 위한 어노테이션이다.
  2. Mock Mvc 객체를 생성하고, Spring Rest Docs 객체를 주입한다.
  3. 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 를 공통 적용하는 부분을 다른 객체에서 관리할 수 있는 부분이 있다.

 

 

 

 

 

리팩토링 목표


  1. 테스트 코드에서 andDo(document()) 부분에서 문서명을 항상 지정해주는 부분
  2. 테스트 코드에서 문서로 보기좋게 설정한 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 만으로도 자동으로 삭제 및 생성할 수 있도록 설정을 추가해주었다.

  1. asciidoctor 를 실행하기 앞서 targetDir 삭제
  2. 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);
    }
}
  1. 우리는 이 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();
    }
}
  1. 리팩토링한 테스트코드에서는 RestDocs Document 를 @AutoConfigureRestDocs 를 통해 자동으로 주입하였으나, 이제는 커스텀해서 사용하기 때문에 해당 어노테이션을 제거합니다.
  2. 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


 

 

 

 

Reference


Comments