기록하기

REST Docs 설정 정리 본문

Server/Spring Boot

REST Docs 설정 정리

jjungdev 2022. 9. 26. 09:34

회사에서 매번 REST Docs 를 설정할 때마다 설정 방법이 헷갈려서 레퍼런스를 많이 찾게 된다. 이미 좋은 자료들이 많긴 하지만 프로젝트마다 그리고 회사의 환경마다 조금씩 설정을 변경해줘야하기 때문에 어려움이 있다.

특히 CI/CD 를 적용해서 젠킨스에 배포를 할 때면 배포 환경에서 MySQL 등에 접속을 막아두었기 때문에 build 시 asciidoctor 가 수행이 되면 안 된다!(이 부분이 제일 힘들었다.. gradle 작성방법이나 문법도 더 공부를 해야겠다..)

그래서 다른 블로그에서 참고한 내용에 좀 더 수정이 필요했기에 해당 내용을 추후 기억하기 위해서라도 적어보려고 한다.

 

프로젝트 환경

  • gradle 7.6
  • Spring Boot 2.7.5
  • Java 11

 

build.gradle 에 REST Docs 관련 설정

plugins {
    id 'org.asciidoctor.jvm.convert' version '3.3.2'' //1)
}

group = 'com.example'
version = '0.0.1' + '-' + new Date().format('yyyyMMdd-HHmmss')
sourceCompatibility = '11'

ext {
    displayVersion = '0.0.1'
    buildTime = getBuildDateTime()

    //2)
    snippetsDir = file('build/generated-snippets')
    docsDir = file('src/docs/asciidoc')
    htmlDir = file('build/docs/asciidoc')
    staticDir = file('src/main/resources/static/docs')
}

def getBuildDateTime() {
    return new Date().format('yyMMdd-HHmm')
}

configurations {
    //3)
    asciidoctorExt
}

repositories {
    mavenCentral()
}

dependencies {
    //4)
    //rest-docs
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
    delete snippetsDir //5)
}

//########################## asciidoctor 설정 ########################## //
asciidoctor {
    //6)
    attributes "date": new Date().format("yyyy-MM-dd"),
            "version": "1.0",
            "developer": "jjung_dev"
    inputs.dir snippetsDir
    dependsOn test
}

asciidoctor.doFirst {
    //7)
    println "===== start asciidoctor"
    delete htmlDir
    delete staticDir
}

asciidoctor.doLast {
    //8)
    println "===== finish asciidoctor"
    task copyDocument() {
        dependsOn asciidoctor
        copy {
            from htmlDir
            into staticDir
        }
    }
}
  • 1) asciidoctor 플러그인 적용
  • 2) snippets, html 파일이 저장될 위치를 선언
  • 3), 4) 의존성 설정 및 설정 명시
  • 5) 테스트 수행시 기존 snippets 삭제
  • 6) 테스트 수행 결과의 snippets 이 저장될 위치 설정
  • 7) asciidoctor 실행 전 기존에 생성된 문서 삭제
  • 8) asciidoctor 실행 이후 staticDir 로 문서 이동

 

설정은 여러 블로그 및 공식 문서를 참고하여 설정을 했고, 해당 내용은 아래 참고사항에 링크를 걸어두었다.

여기서 하나 조금 다른 설정은, build 나 bootJar 수행 시 asciidoctor 가 같이 수행되지 않도록 설정을 했다. 같이 실행하게 되면 배포 환경에서 계속 문제가 생겼기 때문에 이렇게 설정했는데 만약 같이 수행이 되어야 한다면 asciidoctor.doLast 에서 copyDocument 를 수행하는 것이 아닌 가장 하단에 아래 내용을 적어줌으로써 bootJar 시 수행되도록 처리할 수 있다.(아니면 다른 방법이 있다면 댓글에 공유해주셔도 됩니다!)

bootJar {
    dependsOn asciidoctor
    from (htmlDir) {
        into staticDir
    }
}

 

Config 및 CustomSnippet 설정

이제 gradle 설정은 끝났고 Test 수행 시 설정만 더해주면 된다.

프로젝트마다 그리고 사람마다 조금씩 다르겠지만 필자의 경우에는 간단하게만 설정을 해주었다.

 

1. RestDocsConfig

import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;

@TestConfiguration
public class RestDocsConfig {

    @Bean
    public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
        return configurer -> configurer.operationPreprocessors()
                .withRequestDefaults(prettyPrint())
                .withResponseDefaults(prettyPrint());
    }
}

 

이는 TestConfiguration 클래스로, request 나 response 결과를 문서로 보여줄 때 prettyPrint 를 수행하도록 명시해주는 설정 파일이다. 다른 블로그에서는 Util 파일로 만들어서 적용할 테스트에 직접 명시해주기도 하는 것 같다. 혹시 모르니 이 방법도 첨부를 하자면,

 

먼저, Util 파일을 만들고 테스트 수행하는 곳에 import 를 해주면 된다.

import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;

import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;

public interface ApiDocumentUtils {

    static OperationRequestPreprocessor getDocumentRequest() {
        return preprocessRequest(
                modifyUris().scheme("https").host("localhost").port(8081),
                prettyPrint()
        );
    }

    static OperationResponsePreprocessor getDocumentResponse() {
        return preprocessResponse(prettyPrint());
    }
}

 

2. CustomResponseFieldsSnippet

이 설정은 API 호출 결과인 response 를 보여줄 때 custom 하여 필드를 보여주고 싶어 설정을 추가한 내용이다.

데이터 format 이나 description 은 문서화를 위해 좀 더 상세하게 적을 필요가 있다고 생각해 필드를 추가했고 custom 한 설정을 추가했을 때 필요한 설정은 아래와 같다.

 

- CustomResponseFieldsSnippet

import org.springframework.http.MediaType;
import org.springframework.restdocs.operation.Operation;
import org.springframework.restdocs.payload.AbstractFieldsSnippet;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.PayloadSubsectionExtractor;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public class CustomResponseFieldsSnippet extends AbstractFieldsSnippet {

    public CustomResponseFieldsSnippet(String type, PayloadSubsectionExtractor<?> subsectionExtractor,
                                       List<FieldDescriptor> descriptors, Map<String, Object> attributes,
                                       boolean ignoreUndocumentedFields) {
        super(type, descriptors, attributes, ignoreUndocumentedFields, subsectionExtractor);
    }

    @Override
    protected MediaType getContentType(Operation operation) {
        return operation.getResponse().getHeaders().getContentType();
    }

    @Override
    protected byte[] getContent(Operation operation) throws IOException {
        return operation.getResponse().getContent();
    }
}

 

- /src/test/resources/org/springframework/restdocs/templates/response-fields.snippet

|===
|필드명|타입|필수여부|양식|설명

{{#fields}}

|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}}
|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}}
|{{#tableCellContent}}`+{{format}}+`{{/tableCellContent}}
|{{#tableCellContent}}{{description}}{{/tableCellContent}}

{{/fields}}
|===

참고로 아실 수도 있겠지만 해당 디렉토리에 설정을 해야하는 이유는 기본적으로 설정되어 있는 templates 의 디렉토리를 따라서 설정을 해줘야 custom 설정이 적용이 되기 때문이다. 따라서 디렉토리 생성이 필요하며 이렇게 설정을 했을 경우 원하는 response 내용을 담을 수 있다.

 

Test 코드 작성 및 결과

테스트 코드를 작성한 뒤 그 결과를 한 번 살펴보겠다.

테스트는 간단하게 프로젝트 버전을 체크하는 Controller 로 수행을 해보았으며 필자의 경우에는 ControllerTest 라는 추상 클래스를 생성해서 공통적으로 사용하는 설정을 이 곳에서 관리하도록 설계했다. 이 역시 프로젝트마다 조금씩 다르겠지만 지금까지는 이 방법으로 진행을 해왔다.

 

- ControllerTest

import com.fasterxml.jackson.databind.ObjectMapper;
import com.zum.target.internal.api.domain.TargetDataset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs(uriPort = 8081)
@ActiveProfiles("test")
public abstract class ControllerTest {

    @Autowired
    protected MockMvc mockMvc;
    @Autowired
    protected ObjectMapper objectMapper;
 }

 

이외에도 같이 사용하는 변수나 클래스를 이 곳에서 관리해주면 다른 테스트에서 BeforeEach 와 같은 설정을 통해 관리를 할 수 있기 때문에 편리한 부분이 있다.

 

- StatusControllerTest

class StatusControllerTest extends ControllerTest {

    @MockBean
    StatusController statusController;

    @Value("${app.version}")
    private String version;

    @DisplayName("health check")
    @Test
    void healthCheckTest() throws Exception {
        Map<String, String> result = new LinkedHashMap<>();
        result.put("version", version);
        when(statusController.healthCheck()).thenReturn(result);

        mockMvc.perform(get("/status/version"))
                .andDo(document("get/status-version",
                                getDocumentResponse(),
                                responseFields(
                                        fieldWithPath("version").attributes(key("format").value("displayVersion - buildTime(yyyyMMdd-HHmmss) 형식")).description("version 명시"))
                        )
                );
    }
}

여기서는 import 문까지는 필요 없을 것 같아서 제외를 했다. 그리고 MockBean 이나 Mock, Spy 관련 내용은 좀 더 정리를 해서 글을 올리려고 한다.

 

참고로 필자의 경우에는 (1) 테스트 결과인 snippets 를 가지고 -> (2) adoc 문서를 원하는 모습으로 만들어준 뒤 -> (3) 이를 asciidoctor 를 돌려서 html 문서화를 해주었는데 (1) 에서 (2) 단계로 넘어가는 부분이 자동화가 될 수 있을 것 같지만 아직 이것까지는 잘 모르겠다.. 이 부분도 더 학습을 해서 해당 내용을 추가하면 좋을 것 같다.

 

그래서 위 순서대로 진행을 해준 뒤 index.adoc 혹은 각자가 원하는 방식으로 adoc 을 정리해서 asciidoctor 를 수행해주면 짠! 문서가 완성이 된다.

 

 

이렇게 REST Docs 설정에 대해 간략하게 정리를 해보았는데 이후에는 Mock 테스트 관련해서 더 정리를 해보면 좋을 것 같다.

 

 

참고

https://seongtak-yoon.tistory.com/27

 

[Gradle] 파일 복사, Jar에 파일 포함하기 (task copy)

// << 는 바로 수행시키않는 역할을 함 task copySample() << { println file("src/main/resources/sample.properties") println file("build/classes/") copy {  from "src/main/resources/sample.properties"..

seongtak-yoon.tistory.com

https://jinseobbae.github.io/gradle/2022/01/11/gradle-call-task.html

 

[Gradle] fianlizedBy, dependsOn 이용해서 task 자동 실행시키기

gradle task를 작성하다보면 특정 task 이 후 다른 task를 실행시키거나

jinseobbae.github.io