Spring Rest Docs를 적용하기 위해 필요한 코드만 집중적으로 설명합니다.
적용을 하기로 마음먹은 후 가장 첫번째로 build.gradle
의 설정을 바꾸는 일이였다.
즉, 환경설정이라고 이해하면 편할 것 같다.
plugins{
id 'org.asciidoctor.jvm.convert' version '3.3.2' // asciidoctor 플러그인 적용 (asciidoc파일을 변환해주고, Build폴더에 복사해주는 플러그인)
}
configurations {
asciidoctorExt // asciidoctor를 확정하는 asciidoctorExt에 대한 종속성 구성 선언
}
dependencies {
// asciidoctorExt에 spring-restdocs-asciidoctor 의존성 추가
// 이 종속성이 있어야 build/generated-snippets에 있는 .adoc 파일을 읽어 .html 파일로 만들어 낸다.
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
// MockMvc를 사용하기 위한 종속성
// MockMvc 대신 WebTestClient을 사용하려면 spring-restdocs-webtestclient 추가
// MockMvc 대신 REST Assured를 사용하려면 spring-restdocs-restassured 를 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
// 테스트를 통해 생성된 snippets 출력 위치 정의
ext {
snippetsDir = file('build/generated-snippets')
}
tasks.named('test') {
useJUnitPlatform()
// test 스니펫 디렉터리를 출력으로 추가하도록 작업을 구성
outputs.dir snippetsDir
}
// 기존에 존재하던 docs 삭제
// 테스트 할때마다 초기화 과정?
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
// asciidoctor 작업을 구성
asciidoctor {
inputs.dir snippetsDir // 불러올 스니펫 위치
configurations 'asciidoctorExt' // asciidoctorExt에 대한 설정
dependsOn test // test 작업 이후에 실행하도록 설정
}
// 프로젝트를 .jar로 빌드할 때 resodocs가 해당 jar파일에 들어가게끔 하기위한 부분
bootJar {
dependsOn asciidoctor // asciidoctor 작업 이후에 실행하도록 설정
// 생성된 문서(html)를 static/docs로 복사
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
// asciidoctor 작업 이후 생성된 HTML파일을 static/docs로 복사
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("$buildDir/docs/asciidoc")
into file("src/main/resources/static/docs")
}
// copyDocument가 끝난 후 build 실행
build {
dependsOn copyDocument
}
위 설정을 완료하면
Build Flow 는
test코드 수행 -> asciidoctor -> bootJar -> copyDocument -> build
이런 식으로 구성이 될 것이다.
공식문서에는 asciidoctor.doFirst, copyDocument, build 작업이 생략되어 있다.
작업이 생략되어 있으면 build 폴더에서만 .html파일이 생성되고 생략되어 있지 않으면 static/docs 에서도 확인 가능하다.
이어서 테스트 코드에서의 설정을 해보자
@AutoConfigureRestDocs
class exampleTest{
...
// result는 Then단계의 모든 검증이 끝난 ResultActions 객체
result.andDo(
// 문서 작성 시작
MockMvcRestDocumentationWrapper.document( "settlement/using-date", // 스니펫 저장 경로 Ex.build/generated-snippets/settlement/using-date
preprocessRequest( prettyPrint( ) ),
preprocessResponse( prettyPrint( ) ), // request, response JSON 가독성을 위한 설정
queryParameters( // queryParam 설정
parameterWithName( "fixDate" ).description( "지정날짜의 데이터를 가져온다." )
),
responseFields( // response boby에 포함되는 data 설정
fieldWithPath( "error" ).type( JsonFieldType.NULL ).description( "error" ),
// data[] : data : [ {"queryDate": "value"}, {}, ... ]
fieldWithPath( "data[].queryDate" ).type( JsonFieldType.STRING )
.description( "queryDate" ),
fieldWithPath( "data[].settleId" ).type( JsonFieldType.STRING )
.description( "settleId" ),
fieldWithPath( "data[].productCode" ).type( JsonFieldType.STRING )
.description( "productCode" ),
fieldWithPath( "data[].price" ).type( JsonFieldType.STRING )
.description( "price" ),
fieldWithPath( "data[].status" ).type( JsonFieldType.STRING )
.description( "status" ),
fieldWithPath( "data[].cnt" ).type( JsonFieldType.STRING )
.description( "cnt" )
)
)
);
위와 같이 설정 후 build 작업을 수행하면 위에 설정된 스니펫 저장 경로에 기본적으로 6개의 스니펫이 작성된다.
[source,bash]
----
$ curl 'http://localhost:8080/settlement/using-date?fixDate=20230717' -i -X GET
----
$ curl 'http://localhost:8080/settlement/using-date?fixDate=20230717' -i -X GET
[source,http,options="nowrap"]
----
GET /settlement/using-date?fixDate=20230717 HTTP/1.1
Host: localhost:8080
----
GET /settlement/using-date?fixDate=20230717 HTTP/1.1
Host: localhost:8080
[source,http,options="nowrap"]
----
HTTP/1.1 200 OK
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 184
{
"error" : null,
"data" : [ {
"queryDate" : "20230717",
"settleId" : "0",
"productCode" : "0",
"price" : "0",
"status" : "0",
"cnt" : "0"
} ]
}
----
HTTP/1.1 200 OK
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Content-Type: application/json
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 184
{
"error" : null,
"data" : [ {
"queryDate" : "20230717",
"settleId" : "0",
"productCode" : "0",
"price" : "0",
"status" : "0",
"cnt" : "0"
} ]
}
[source,bash]
----
$ http GET 'http://localhost:8080/settlement/using-date?fixDate=20230717'
----
$ http GET 'http://localhost:8080/settlement/using-date?fixDate=20230717'
output-directory/index/request-body.adoc
위 코드에서는 비어있다.
output-directory/index/response-body.adoc
[source,json,options="nowrap"]
----
{
"error" : null,
"data" : [ {
"queryDate" : "20230717",
"settleId" : "0",
"productCode" : "0",
"price" : "0",
"status" : "0",
"cnt" : "0"
} ]
}
----
{
"error" : null,
"data" : [ {
"queryDate" : "20230717",
"settleId" : "0",
"productCode" : "0",
"price" : "0",
"status" : "0",
"cnt" : "0"
} ]
}
|===
|Parameter|Description
|`+fixDate+`
|지정날짜의 데이터를 가져온다.
|===
Parameter | Description |
---|---|
fixDate | 지정날짜의 데이터를 가져온다. |
|===
|Path|Type|Description
|`+error+`
|`+Null+`
|error
|`+data[].queryDate+`
|`+String+`
|queryDate
|`+data[].settleId+`
|`+String+`
|settleId
|`+data[].productCode+`
|`+String+`
|productCode
|`+data[].price+`
|`+String+`
|price
|`+data[].status+`
|`+String+`
|status
|`+data[].cnt+`
|`+String+`
|cnt
|===
Path | Type | Description |
---|---|---|
error | Null | error |
data[].queryDate | String | queryDate |
data[].settleId | String | settleId |
data[].productCode | String | productCode |
data[].price | String | price |
data[].status | String | status |
data[].cnt | String | cnt |
테스트가 끝나고 생성된 .adoc 스니펫을 모아 완전한 API 명세서로 만들어보자
src/docs/asciidoc 하위에 완전한 명서세가 될 index.adoc 와 각 API를 문서화 한 exampl.adoc 파일들이 필요하다.
src
asciidoc
index.adoc : 하위 API문서들을 합친 최종 명세서
example1
example1.adoc
example2
example2.adoc
= Document Title (문서 제목)
Kismet R. Lee <kismet@asciidoctor.org> (작성자 라인)
:description: The document's description. (문서속성에 메타데이터를 할당하는 속성 항목)
:sectanchors: (내장 문서 속성을 설정하는 속성 항목)
:url-repo: https://my-git-repo.com (사용자 정의 문서 속성에 값을 할당하는 속성 항목)
:source-highlighter: highlightjs (문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용)
:toc: left (toc (Table Of Contents)를 문서의 좌측에 두기)
:toclevels: 2 (toc의 깊이 설정)
// 빈줄은 문서 헤더의 끝을 나타낸다.
[[]] : 링크걸기
The document body starts here.
// 생성된 스니펫의 경로를 불러온다.
ifndef::snippets[]
:snippets: ../build/generated-snippets
endif::[]
= 서버 API 목록
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
include::src/docs/asciido/cexampl1/exampl1.adoc[]
include::src/docs/asciidoc/exampl2/exampl2.adoc[]
asciidoctor 작업을 실행하면 html 문서를 생성할 수 있다. -> arc/docs/asciidoc 에 생성한 .adoc 파일을 기반으로...
build.gradle > bootJar 작업을 asciidoctor 작업 이후에 실행되도록 하여 asciidoctor 작업이후 생성된 .html 파일을 build/docs/asciidoc 안에 복사한다.
build.gradle 를 필자와 같이 설정해 놓았다면
build/docs/asciidoc/index.html
src/main/resources/static/docs/index.html
위 2경로에 .html 파일을 확인할 수 있다.
localhost:8080/docs/index.html
애플리케이션을 실행 후 위 리소스로 접근하면 최종적으로 생성된 API문서를 볼 수 있다.
적용하며 여러 공식문서, 블로그를 찾아봐도 뭔가 안맞는? 부분이 있어 난감했지만...극복...!
필자의 포스트를 따라 적용하며 문제가 생기면 댓글로 물어봐 주시면 최대한 도움 드리겠습니다!
다음 포스트는 Spring Rest Docs 와 Swagger-UI 연동 적용기로 작성해봐야겠다.
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started
https://github.com/springdoc/springdoc-openapi
https://docs.asciidoctor.org/asciidoc/latest/
좋은 글 감사합니다. 자주 올게요 :)