Spring Rest Docs를 적용하기 위해 필요한 코드만 집중적으로 설명합니다.

Project Spec

  • gradle 8.1.1
  • Spring Boot 3.0.6
  • java 17

적용을 하기로 마음먹은 후 가장 첫번째로 build.gradle의 설정을 바꾸는 일이였다.
즉, 환경설정이라고 이해하면 편할 것 같다.

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" )  
		)  
	) 
);

테스트 실행 및 .adoc 스니펫 생성

위와 같이 설정 후 build 작업을 수행하면 위에 설정된 스니펫 저장 경로에 기본적으로 6개의 스니펫이 작성된다.

  • output-directory/index/curl-request.adoc
[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
  • output-directory/index/http-request.adoc
[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
  • output-directory/index/http-response.adoc
[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"
  } ]
}
  • output-directory/index/httpie-request.adoc
[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"
  } ]
}
  • query-parameters.adoc (위 코드에 따라 추가된 스니펫)
|===  
|Parameter|Description  
  
|`+fixDate+`  
|지정날짜의 데이터를 가져온다.  
  
|===
ParameterDescription
fixDate지정날짜의 데이터를 가져온다.
  • response-fields.adoc (위 코드에 따라 추가된 스니펫)
|===  
|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  
  
|===
PathTypeDescription
errorNullerror
data[].queryDateStringqueryDate
data[].settleIdStringsettleId
data[].productCodeStringproductCode
data[].priceStringprice
data[].statusStringstatus
data[].cntStringcnt

생성된 스니펫들로 asciidoc 문서 생성

테스트가 끝나고 생성된 .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[]

HTML 문서 생성(생성된 API 문서를 애플리케이션에서 보여주기)

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 연동 적용기로 작성해봐야겠다.

Reference

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#getting-started
https://github.com/springdoc/springdoc-openapi
https://docs.asciidoctor.org/asciidoc/latest/

profile
응애 나 애기 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 1일

좋은 글 감사합니다. 자주 올게요 :)

답글 달기
Powered by GraphCDN, the GraphQL CDN