Spring REST Docs 기본 설정과 API 문서 만들어보기

Dae-Hwa Jeong·2021년 11월 2일
4

Spring REST Docs

목록 보기
1/3

Spring REST Docs는 RESTful 서비스 문서화에 사용된다. 인수테스트나 컨트롤러 테스트와 같은 엔드포인트 테스트에서 사용할 수 있다. 각 테스트에서 스니펫이라고 하는 문서 조각을 만들어주는데 이를 모아서 하나의 문서로 꾸밀 수 있다.

문서는 adoc라는 확장자를 사용하며 AsciiDoc 문법을 따른다. 때문에 러닝커브가 존재하고 단순 구현에도 시간이 소요된다는 단점이 있다. 반면, 반드시 테스트를 통과해야 문서가 생성되기 때문에 서비스 자체의 신뢰도가 높아진다. 즉, 단순히 문서화에만 관심이 있다면 다른 도구를 사용하는 것이 더 나을 수 있다.

기본 설정

Gradle 6 + Spring Boot 2.5.3 + JUnit5 + REST Assured를 사용하는 환경에 적용시켰다. 이외에도 MockMvc나 WebTestClient와 조합하여 사용할 수 있다. JUnit5외의 테스트 프레임워크도 지원한다고 한다.

build.gradle 설정

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'org.asciidoctor.convert' version '1.5.9.2'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

ext {
    // 스니펫 생성 위치 변수에 저장
    set('snippetsDir', file("build/generated-snippets"))
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'io.rest-assured:rest-assured'
    
    /* Rest Docs */
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
}

test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}


asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

task copyAsciidoctor(type: Copy) {
    dependsOn asciidoctor
    from "${asciidoctor.outputDir}/html5"
    into "${sourceSets.main.output.resourcesDir}/static/docs"
}

bootRun {
    dependsOn copyAsciidoctor
}

Gradle 버전은 6을 사용하는게 좋다. 7 버전은 문서 생성 과정에서 아래와 같은 에러가 발생한다. 다른 우회방법이 있지만 복잡하다. 반드시 7버전을 사용해야하는 것이 아니면, 공식적으로 대응해줄때까지 기다리는게 좋아보인다.

Some problems were found with the configuration of task ':asciidoctor' (type 'AsciidoctorTask').
  - In plugin 'org.asciidoctor.convert' type 'org.asciidoctor.gradle.AsciidoctorTask' method 'asGemPath()' should not be annotated with: @Optional, @InputDirectory.

./gradle/wrapper/gradle-wrapper.properties에서 해당 프로젝트의 gradle 버전을 변경할 수 있다.

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.1-bin.zip // 이 부분을 바꿔준다.
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

테스트 클래스 설정

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(RestDocumentationExtension.class)
class SimpleAcceptanceTest {

    private static final String BASE_URL = "http://localhost";

    @LocalServerPort
    private int port;

    protected RequestSpecification spec;

    @BeforeEach
    void setUpRestDocs(RestDocumentationContextProvider restDocumentation) {
        this.spec = new RequestSpecBuilder()
                .addFilter(documentationConfiguration(restDocumentation))
                .build();
    }
}

스니펫

말 그대로 문서 조각이다. 이를 이용하여 문서에 사용될 객체의 구조를 정의하고 테스트 과정에서 확인한 뒤 adoc 파일을 만들어준다. 스니펫 객체가 실제 요청, 응답에 사용되는 객체와 다르면 테스트 통과가 되지 않는다. 즉, 엔드포인트에 사용되는 객체의 구조를 검증하는 동시에 테스트 문서를 만드는 것이다.

참고

스니펫 객체 만들기

아래와 같이 스니펫 객체를 만들 수 있다.

private Snippet simplePathParameterSnippet() {
    return pathParameters(parameterWithName("id").description("아이디"));
}

private Snippet simpleRequestParameterSnippet() {
    return requestParameters(parameterWithName("name").description("이름"));
}

private Snippet simpleResponseFieldsSnippet() {
    return responseFields(
            fieldWithPath("id")
                    .type(JsonFieldType.NUMBER)
                    .description("아이디"),
            fieldWithPath("name")
                    .type(JsonFieldType.STRING)
                    .description("이름")
    );
}

documentation 필터 만들기

이를 이용해 Restassured documentation 필터를 만들어 테스트에 넣어주면, 명시한 디렉토리에 adoc파일을 만들어준다.

@Test
void read() {
    RestDocumentationFilter restDocumentationFilter = document(
            // identifier, 이를 이용해 adoc파일을 저장할 디렉토리를 생성한다 
            "{class_name}/{method_name}/",
            simplePathParameterSnippet(),
            simpleRequestParameterSnippet(),
            simpleResponseFieldsSnippet()
    );

    RequestSpecification given = RestAssured.given(this.spec)
        .baseUri(BASE_URL)
        .port(port)
        .pathParam("id", 1)
        .queryParam("name", "name")
        // 필터를 넣어준다.
        .filter(restDocumentationFilter);

    Response actual = given.when()
                           .get("/simple/{id}");

    actual.then()
          .statusCode(HttpStatus.OK.value())
          .log().all();
}

asciidoctor 실행

build.gradle에 만들어둔 asciidoctor task를 실행시킨다.

asciidoctor task

build.gradle에 지정해준 snippetsDir의 하위 디렉토리에 adoc파일이 생성됐다.

snippets

참고 - 기본 생성 스니펫

API 문서 만들기

스니펫은 말 그대로 문서 조각이기때문에 스니펫을 모아주는 문서를 작성해야 API문서가 완성된다. adoc 문서를 만들고 서버를 띄워서 확인해보자.

참고

adoc 문서 작성

adoc파일들을 모아서 문서를 작성해보자. 위에서 말했듯 adoc 파일은 AsciiDoc 문법을 따르는데, 마크다운과 유사하지만 include 기능을 가지고 있다. 단순히 링크를 거는 것이 아니라, 해당 페이지 내에 붙여넣을 수 있다.

src/docs/asciidoc/ 디렉토리를 만들어 adoc파일을 만들어보자.

adoc document

include:: 문법을 이용해 스니펫을 불러올 수 있다.

// index.adoc
= Rest Docs Example

== Simple Service

=== curl request

include::{snippets}/simple-read/curl-request.adoc[]

=== Path parameters

include::{snippets}/simple-read/path-parameters.adoc[]

=== Request parameters

include::{snippets}/simple-read/request-parameters.adoc[]

=== Response Fields

include::{snippets}/simple-read/response-fields.adoc[]

=== HTTP request

include::{snippets}/simple-read/http-request.adoc[]

=== HTTP response

include::{snippets}/simple-read/http-response.adoc[]

AsciiDoc 플러그인을 이용하면 편리하다.

asciidoctor: WARNING: dropping line containing reference to missing attribute: snippets 와 같은 에러메세지가 뜨면 spring-restdocs-asciidoctor 의존성이 제대로 정의되어있는지 확인해보자.

asciidoctor task를 실행시키면 build/asciidoc/html5/index.html이 생성된다.

html document

adoc 파일 명이 index.adoc이기 때문에 index.html로 생성된 것이다.

서버 실행

build.gradlecopyAsciidoctor를 정의해놨다. 이는 위에서 만들어진 index.html을 정적 파일 디렉토리로 복사해주는 작업이다. bootRun에서 실행되게 해놨으니 bootRun으로 서버를 실행시켜보자

html in static dir

빌드 결과의 resourcesindex.html이 복사됐다. 이외에도 필요한 작업에 copyAsciidoctor를 실행시켜주는 등의 방법으로 복사해주면 된다.

디렉토리 구조를 보면 알겠지만, /docs/index.html로 요청하면 API문서를 볼 수 있다.

api document

그래서 좋은점은?

따로 설정을 해줘야하고 문서를 만드는데 시간도 꽤 든다. 복잡한 구조라면 스니펫 만드는 것 부터 만만치 않다. 하지만 테스트를 통과해야만 문서가 작성된다는 점이 큰 장점이라 생각한다. 스웨거를 이용하면 쉽게 문서화시키고 테스트도 테스트 문서에서 바로 해볼 수 있다. 하지만 잘 생각해보면 포스트맨으로 일일이 확인하는거랑 크게 차이가 없다. 그리고 복잡한 구조 혹은 자세한 표현을 하려면 스웨거에도 복잡한 설정이 들어갈 수 밖에 없다.

현재 진행중인 프로젝트에서 인수테스트를 하고 있지만 응답으로 오는 json객체 구조를 검증하는게 쉽지 않았다. 예를 들어, 단순히 객체만으로 비교를 하는 경우 필드명이 스네이크케이스로 되어야 하는데 카멜케이스로 json객체를 만들고 있는 것을 인지하기 어렵다. 하지만, 스니펫을 이용하여 사전에 구조를 검증하게 되면 이런 불상사가 줄어든다. RestDocs를 사용하지 않더라도 구조 검증을 하려면 스니펫을 만드는 것과 비슷한 노력을 들여야 한다. 이왕 검증할거면 테스트 문서도 함께 만들 수 있다는 점이 큰 장점이라 생각된다.

References

profile
대화로그

0개의 댓글