REST Docs "어딜 보시는 거죠? 그건 제 잔상입니다만?"

ohzzi·2022년 7월 29일
5

많은 프로젝트 팀들이 API 문서화를 위해 Spring REST Docs를 사용합니다. 그리고 REST Docs를 만드는 과정에서 생긴 html 파일을 서버에 함께 배포해서 사용합니다. 저희 F12팀 역시 http://{백엔드ip}:{백엔드포트}로 접속하면 docs가 뜨도록, 즉 index.html(스프링부트의 기본 설정이니까요)로 REST Docs가 뜨도록 설정해주었습니다. 그렇게 하기 위해 build.gradlecopyDocument라는 task를 작성해주었습니다.



test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

asciidoctor {
    configurations 'asciidoctorExtensions'
    inputs.dir snippetsDir
    sources {
        include("**/index.adoc")
    }
    baseDirFollowsSourceFile()
    dependsOn test
}

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("${asciidoctor.outputDir}")
    into file("src/main/resources/static")
}

bootJar {
    dependsOn copyDocument
}

자세히 설명하자면, 테스트 진행 이후 asciidoctor가 REST Docs를 만드는 작업을 진행하고, 작업이 끝나면 그 결과물로 나오는 index.htmlsrc/main/resources/static 디렉토리로 복사해서 스프링을 띄우면 기본 페이지로 REST Docs를 보여줄 수 있도록 설정하는 스크립트입니다. 그런데 실제 배포를 해보니, REST Docs가 뜨지 않았습니다.

Whitelabel Error Page가 뜨다 보니 아무래도 index.html이 없어서 그렇구나 라고 생각했습니다. 그래서 혹시 파일 복사가 제대로 되지 않는 것은 아닌지 로컬에서 ./gradlew clean bootJar 명령으로 빌드를 다시 해보니, 의도한대로 src/main/resources/static 폴더에 index.html 파일이 제대로 복사되는 것을 확인할 수 있었습니다. 그리고 나서 인텔리제이로 스프링부트를 실행해서 메인 페이지로 들어가 봤는데, 정상적으로 REST Docs를 볼 수 있었습니다. 하지만 서버에서는 REST Docs를 볼 수 없었습니다.

왜 갑자기 이렇게 된 걸까요? 처음에는 Jenkins에 배포 스크립트를 짤 때 문제가 생겼다고 생각했습니다. 이 대머리 할아버지와 함께하는 시간 동안 실수를 한 두가지를 한 것이 아니었거든요.

Jenkins 하다보니 할아버지 따라서 머리가 빠지겠네요.

하지만 계속 빌드해보고 로그를 확인하면서 분석해보니, Jenkins 문제는 아니었습니다. 왜냐면, 잘 되고 있다고 생각하던 로컬에서조차도 사실 REST Docs는 빌드에 포함이 안되고 있었기 때문입니다. 무슨 소리세요? 로컬에서 REST Docs 볼 수 있다면서요? 라고 하시겠지만, 그건 인텔리제이로 스프링부트를 띄우기 때문이었습니다. 만약 ec2에 배포하는 것 처럼 직접 jar 파일을 실행했다면, REST Docs를 볼 수 없었을 것입니다.

사실 우리가 보고 있었던 건 REST Docs의 잔상입니다만?

물론 jar 파일을 실행해도 REST Docs를 볼 수는 있었습니다. 왜냐면 resources/staticindex.html 자체는 복사되기 때문에 다시 한번 빌드를 하면 빌드에 index.html은 포함되었기 때문입니다. 하지만 이 index.html은 과연 올바른 파일일까요? 만약 같은 빌드를 다시 한 번 하지 않고, 새로운 수정 사항이 생겨서 새 빌드를 했다고 가정하겠습니다. 기존 버전을 v1, 새 버전을 v2라고 하면 서버에는 v2 코드가 올라가지만 서버에서 볼 수 있는 REST Docs는 v1일겁니다. 즉, 우리는 과거의 REST Docs를 보고 있는 것이죠.

사실 과거의 REST Docs를 보는 것 조차 기존 workspace에 복사된 html 파일이 남아있기 때문에 생긴 일일 뿐입니다. 저희 팀은 Jenkins가 올라가 있는 EC2의 디스크 용량을 아끼기 위해 빌드가 끝나면 workspace를 지워주는데요, 이렇게 했더니 아무리 빌드를 해도 REST Docs를 서버에 띄울 수 없었습니다.

자 그렇다면 우리는 왜 REST Docs의 잔상만 보고 있었을까요?

build.gradle의 태스크 진행 순서

저는 한 가지 가설을 세웠습니다. index.html이 정상적으로 복사는 되는데 서버에서는 볼 수 없다면, 빌드하는 과정에서 src/main/resources/static/index.html을 포함하지 않는다는 가설을 말이죠. 그래서jar -tf 명령어로 서버에 배포된 jar 파일의 내부 파일 목록을 확인해보았습니다. 만약 index.html이 함께 빌드되었다면 BOOT-INF/classes/static/index.html이 있어야 하겠지만, 찾아볼 수 없었습니다. 즉, 제 가설대로 제대로 빌드가 되고 있지 않았습니다. 그래서 빌드 스크립트를 처음부터 짚어나가며 확인해본 결과, 빌드 과정에서 문제가 있는게 맞았습니다.

우선 저희가 작성한 build.gradle 태스크를 확인해봅시다. dependsOn으로 순서를 지정해주었기 때문에, test -> asciidoctor -> copyDocument -> bootJar 순서대로 작업을 진행합니다. 이제 로그를 보겠습니다.

Starting a Gradle Daemon (subsequent builds will be faster)
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :compileTestJava
> Task :processTestResources
> Task :testClasses
> Task :test
...
> Task :asciidoctor
> Task :copyDocument
> Task :bootJar

지정한대로 test -> asciidoctor -> copyDocument -> bootJar의 순서는 유지되고 있었습니다.

여기서 짚고 넘어가야 할 부분이 있습니다. 테스트에 관련된 작업을 하기 전 processResources 작업을 진행한다는 점입니다. processResourcessrc/main/resources 디렉토리 내의 파일을 build/resources/main으로 복사합니다. 그런데, REST Docs 생성 과정과 생성된 문서의 복사 과정은 test 태스크가 끝난 이후에 진행됩니다. 당연하게도 테스트 없이는 REST Docs를 만들 수 없기 때문입니다.

jar 파일을 만들어주는 마지막 태스크인 bootJar 시점에서 보겠습니다. bootJar 이전에 copyDocument 태스크가 진행되었으니 REST Docs 결과물이 src/main/resources/static 디렉토리에는 들어가 있습니다. bootJar의 빌드에 포함시키려면 build의 하위 디렉토리, 즉 여기서는 build/resources/main/static 아래에 index.html이 들어가야 합니다. 하지만 src에만 파일이 있고 build로는 복사하는 과정을 거치지 않았기 때문에 jar를 만드는 과정에서는 index.html이 포함되지 않습니다. 이렇게 만들어진 jar 파일을 배포하기 때문에 서버에는 REST Docs가 배포되지 않습니다.

그래서 이 상태로 다시 빌드를 하면, src/main/resources/static 아래에 있는 index.htmlprocessResources 태스크에서 build 쪽으로 옮겨지게 되어 jar 파일에 포함되게 됩니다. 다만 이 파일은 이전 빌드에서 복사된 파일이기 때문에, 서버에 정상적으로 배포가 된 것 처럼 보여도 우리는 과거의 REST Docs를 보게 됩니다.

해결책: jar 파일을 만들 수 있도록 직접 넣어주자

저희 팀은 그래서 copyDocument의 목적지를 조금 수정했습니다. 어차피 src/main/resources/static 아래의 index.html은 인텔리제이로 스프링부트를 띄워서 확인 할 때나 필요하기 때문입니다.

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("${asciidoctor.outputDir}")
    into file("build/resources/main/static")
}

저는 src 아래에도 index.html이 있으면 좋겠는데요?라고요? 그렇다면 태스크 하나를 추가해주시면 됩니다.

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("${asciidoctor.outputDir}")
    into file("src/main/resources/static")
}

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

bootJar {
    dependsOn buildDocument
}

아직 gradle에 익숙하지 않아 설명이 틀린 부분이 있을 수 있습니다. 해당 부분에 대해서는 지적해주시면 수정하도록 하겠습니다!

profile
배울 것이 많은 초보 개발자 입니다!

2개의 댓글

comment-user-thumbnail
2022년 7월 29일

구독 좋아요 댓글까지 남깁니다~

답글 달기
comment-user-thumbnail
5일 전

멋지다 오찌

답글 달기