[Spring] Spring rest docs 적용기(gradle 7.0.2)

Junseo Kim·2021년 7월 6일
22

Spring

목록 보기
4/7
post-thumbnail
post-custom-banner

⚠️이 글은 Spring Rest Docs의 개념에 대해 정리한 글이 아닌 Spring Rest Docs를 적용하고, 발생한 문제에 대해 정리한 글입니다.

github ➜ https://github.com/KJunseo/spring-rest-docs


⚙️ 환경

  • Spring Boot 2.5.2
  • Gradle 7.0.2
  • Java 11
  • junit5
  • MocvMvc

⚙️ build.gradle

아래의 build.gradle에 rest docs 기능을 추가해보려고 한다.

plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

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

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'com.h2database:h2:1.4.199'
}

test {
    useJUnitPlatform()
}

plugin 추가

gradle 7 부터는 org.asciidoctor.convert가 아닌asciidoctor.jvm.convert를 사용해야한다.

이 플러그인은 adoc 파일 변환, build 디렉토리에 복사하기 위한 역할을 한다.

plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

스니펫 디렉토리 설정

ext는 전역변수를 설정해주는 것이다. gradle은 build/generated-snippets에 스니펫이 생성되므로, 스니펫 생성 디렉토리를 변수에 담아준다.

ext {
    snippetsDir = file('build/generated-snippets')
}

asciidoctor 추가

asciidoctor 설정. 기존에 존재하는 docs를 삭제해준다.

asciidoctor {
    dependsOn test
    inputs.dir snippetsDir
}

asciidoctor.doFirst {
    delete file('src/main/resources/static/docs')
}

bootJar 추가

bootJar 설정. 스니펫을 이용해 문서 작성 후, build - docs - asciidoc 하위에 생기는 html 파일을 BOOT-INF/classes/static/docs로 복사해준다.

bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'BOOT-INF/classes/static/docs'
    }
}

copyDocument

build/docs/asciidoc 파일을 src/main/resources/static/docs로 복사해준다.

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

의존성 추가

나는 테스트 코드를 mockMvc를 통해 작성해서 mockmvc의존성을 넣어줬다. restassured를 사용하려면 restassured 의존성을 넣어주면된다.

dependencies {
    // ...
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

최종 build.gradle

compileJava -> test -> asciidoctor -> bootJar 순으로 실행된다.

plugins {
    id 'org.springframework.boot' version '2.5.2'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "org.asciidoctor.jvm.convert" version "3.3.2"
}

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

ext {
    snippetsDir = file('build/generated-snippets')
}

bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'BOOT-INF/classes/static/docs'
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'mysql:mysql-connector-java'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'com.h2database:h2:1.4.199'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

test {
    useJUnitPlatform()
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

asciidoctor.doFirst {
    delete file('src/main/resources/static/docs')
}

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

build {
    dependsOn copyDocument
}

📝 테스트 코드 작성

이제 테스트 코드를 작성해주면, 통과된 테스트 코드에 따라 스니펫이 생성된다.
나는 MockMvc를 사용했다. MockMvc의 경우 @WebMvcTest로 테스트가 가능하다(restassured의 경우 @SpringBootTest로 해야함). @AutoConfigureRestDocs(rest docs 관련 설정)같은 어노테이션도 붙여준다.

// 예시 
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@WebMvcTest(ReviewController.class)
@AutoConfigureRestDocs
class ReviewControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    @DisplayName("컨택하기")
    void newReview() throws Exception {
        String body = objectMapper.writeValueAsString(
                new ReviewCreateRequest(1, 2, "hello world", "abcdefghi", "github/kjunseo"));

        mockMvc.perform(
                post("/reviews").content(body)
                                .contentType(MediaType.APPLICATION_JSON))
               .andExpect(header().string("Location", "/reviews/1"))
               .andExpect(status().isCreated())
               .andDo(document("reviews/create",
                       responseHeaders(headerWithName("Location").description("review detail resource id"))
               ));
    }
}

이런식으로 테스트를 작성 후, 테스트가 성공하면, build/generated-snippets 하위에 스니펫이 생성된다.

📝 html api문서 만들기

생성된 파일을 묶어 html api문서로 만들기 위해서 gradle인 경우 src/docs/asciidoc 하위에 adoc 파일을 만들어줘야한다.(네이밍은 자유!)

만들어준 파일에 생성된 스니펫을 이용해서 문서를 작성하면 된다.

// 예시 
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

== REQUEST

include::{snippets}/reviews/create/http-request.adoc[]

== RESPONSE

include::{snippets}/reviews/create/http-response.adoc[]

== REQUEST

include::{snippets}/reviews/student-search/http-request.adoc[]

== RESPONSE

include::{snippets}/reviews/student-search/http-response.adoc[]

그 후 ./gradlew build 해주면 resources - static - docs 하위에 문서가 생긴다.

🚀 trouble shooting

build 하위에 docs 디렉토리가 생기지 않는 경우

공식 문서나 많은 블로그 글을 따라 진행해봤을때, build 하위에 docs 디렉토리가 생기지 않는 경우가 발생했다. 결론부터 말하면 버전 문제였다.

현재 나는 스프링 부트 2.5, gradle 7 버전을 사용하고 있는데, 이 경우에는 공식문서에 나와있는대로 진행하면 docs 디렉토리가 생기지 않는다.

gradle 7부터는 id 'org.asciidoctor.convert' version '1.5.6' 대신 id "org.asciidoctor.jvm.convert" version "3.3.2"를 사용해야한다.

Unresolved directive in api.adoc - include::

src - docs - asciidoc 하위의 adoc 문서에는 스니펫이 잘 적용되어도, build - docs - asciidoc 하위의 html에서 Unresolved directive in api.adoc - include::스니펫 오류가 발생했다.

생성된 스니펫 경로를 읽어오지 못해서 발생하는 문제였다.

인터넷에서는 adoc문서 상단에 아래 구문을 적어주면 된다고했으나 여전히 스니펫을 인식하지 못했다.

ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]

경로를 수정하여 아래와 같이 해주니까 정상 작동했다. 아직 정확히 왜 해결된건지는 모르겠다. 😭

ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

빌드 후 최신 반영이 안되는 경우

resources - static - docs 하위의 html(build - docs - asciidoc 하위에 만들어지는 html 파일이 복사된 파일)이 build해도 최신화가 되지 않는 문제를 겪었다.

resources - static - docs 하위 html 파일이 뭔가 캐싱된 값이 남아있는 느낌이다.

빌드를 두 번해야 최신화가 적용되었다.

build.gradle의 bootJar 쪽 설정과 관련이 있나? 라는 생각이 들긴하지만 아직 해결하지 못했다.

해결했다.. 이유는 잘 모르겠지만, asciidoctor.doFirst를 사용하여 기존에 존재하는 문서를 지워준 후, copyDocument로 직접 복사해주니까 최신화가 완료되었다.

커스텀 스니펫 적용

커스템 스니펫을 적용하려고 찾아보다가
src/test/resources/org/springframework/restdocs/templates
하위에 .snippet 파일을 만들면 적용된다는 글을 보고 해보았으나 실패했다.

좀 더 찾아본 결과 asciidoctor라는 디렉토리를 하나 더 만들어야했다.
src/test/resources/org/springframework/restdocs/templates/asciidoctor

이 하위에 스니펫 파일을 만드니까 정상 작동했다.

pathVariable 관련 이슈

pathVariable을 rest docs로 문서화 할때 urlTemplate~~ 오류가 발생했다.

결론부터 말하면 mockMvc.perform을 할때 get(), post() 같은 메서드가 문제였다.

기본적으로 MockMvcRequestBuilders.get()을 많이 사용하였는데, pathParameters를 문서화 해주려면 RestDocumentationRequestBuilders.get()을 써줘야 한다.

post-custom-banner

8개의 댓글

comment-user-thumbnail
2021년 7월 9일

덕분에 문제 해결했습니다.. 정말 멋진 분이시네요!!!

1개의 답글
comment-user-thumbnail
2021년 7월 13일

너무 섹시해요

1개의 답글
comment-user-thumbnail
2022년 3월 3일

혹시 bootJar task를 수행하면 간헐적으로 build/docs/xxx.html 문서가 생성되지 않아서 jar 파일에 포함되지 않는 경우 없으셨나요?
제가 확인한바로는 이전에 빌드한 결과물이 build 디렉토리에 남아 있으면 BOOT-INF/classes/static/doc 디렉토리에 정상적으로 copy되고 jar 패키징에 포함 되는데, clean 후에 bootJar를 수행하면 BOOT-INF/classes/static/doc 디렉토리에 생성되지 않고 마찬가지로 jar 파일 내부에도 포함되지 않아서 애플리케이션을 실행했을 때 문서가 존재하지 않네요.
문서 파일이 생성되기 전에 copy를 수행해서 그런게 아닐까 생각하는데.. 이런 적 있으셨나요?

1개의 답글
comment-user-thumbnail
2022년 4월 1일

스니펫 경로 오류 겪었는데 덕분에 해결했습니다. 감사합니다. 행복하세요!

1개의 답글