우리가 Api를 개발하고 이에 대한 스펙을 다른이에게 공유하기에 앞써 우리는 Api문서라는 것을 만들어야한다. Api 문서를 만드는 데에는 직접 markdown을 작성하거나 postman을 이용하거나 등등의 여러 방법이 존재하지만 오늘은 RestDocs에 대해서 이야기 해보려고 합니다.
왜 Rest Docs인가?
그럼 왜 굳이 Postman처럼 편한 방법이 있는데 다른거를 쓰려고 할까? 자동화라는 점도 매우 큰 장점이지만 테스트 케이스를 강제한다는 것이 매우 큰 이점이자 매력인거 같다. 테스트를 만들고 코드를 만들어야한다는 점은 누구나 알지만 바쁘다 보면 혹은 귀찮다는 이유로 잘 안 만들게 되는게 사실인 것 같다. 주변에 다른 개발자 분들께 물어보면 만들어야하는데 하면서 가끔은 안 만드신다고 하신다. 이런 점에서 Rest Docs로 API문서를 만드는거 매우 큰 이점이자 선생님 같은 존재가 된다.
즉, RestDocs로 API문서를 만들기 위한 요구조건으로 테스트 케이스가 반드시 필요하기 때문에 이를 이용해서 개발을 하면 귀찮지만 좋은 코드를 만들 수 있게 도움을 줄 수 있는듯 하다!
Rest Docs 환경 설정하기
gradle기반의 환경 설정을 이야기할 것이니 maven은 나중에 공부하고 더 추가하겠다...
먼저 gradle에 snippesDir을 다음과 같이 설정한다.
1. snippetsDir 설정하기
ext {
snippetsDir = file('build/generated-snippets')
}
이게 왜 필요한지 먼저 테스트 케이스를 보고 설명하겠다.
우리가 Api에 대한 내용을 만들고 이를 rest docs에 적용하기 위해서는 adoc이라는 걸 만들어야한다. adoc이란 asciidoc의 확장자인데 우리의 restdocs는 이러한 adoc을 테스트 케이스의 request, response마다 만들어준다. 우리가 설정한다면 말이다. 그런 adoc들의 파일 설정에 있어서 root 경로가 되는 게 저 snippetsDir이다.
2. bootjar 설정하기
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'BOOT-INF/classes/static/docs'
}
}
이는 우리가 나중에 프로젝트 전체를 jar파일로 만들 때 restdocs가 해당 jar파일에 들어가게끔 하기위한 부분으로 boot 실행 시에 asciidoctor.outputDir에 있는 html 파일을 BOOT-INF/classes/static/docs의 경로 아래로 복사해주는 역할을 한다.
3. dependency 설정
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
dependencies 하위에 아래 설정을 추가해주자. 이는 테스트 코드를 수행할 때 적용되는 역할로 테스트 수행시에 restdocs가 implementation되는 것을 볼 수 있다.
4. asciidoctor 적용
asciidoctor가 무엇이냐,
역시 공홈에 가서 자기들의 설명을 읽는게 최고다. 그냥 adoc파일을 html등과 같은 파일로 파싱해주는 역할을 해주는 coverter라고 생각하면 된다.
asciidoctor {
dependsOn test
inputs.dir snippetsDir
}
먼저 이 부분은 dependsOn test라는 부분은 test 시에만 실행이 되게 하는 것을 알 수 있다. 또한 inputs.dir을 통해 asciidoctor가 사용할 input파일들의 위치가 사전에 정해준 snippetsDir에 존재하는 것을 알 수 있다.
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
이후에 우리가 asciidoctor가 수행되기 전에 그 전에 만들어졌던 restdocs에 대한 삭제 과정을 진행하게 한다. 이는 여러 restdocs가 생기는 것을 방지해준다.
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
우리가 프로젝트를 실행하면 프로젝트 {address}/docs/{restdocs이름} (ex, http://localhost:8080/docs/testPractice.html)으로 해당 restdocs에 대한 접근이 가능해져야한다. 그러기 위해서는 우리가 만든 html이 src/main/resources/static에 존재해야하는데 이를 위해 만들어진 task라고 보면 된다.
이렇게 만들어진 task를 아래와 같이 명시해서 build시 수행되도록 한다.
build {
dependsOn copyDocument
}
자 이제 이렇게 만들어진 gradle의 전체 모습은 다음과 같다.
plugins {
id 'org.springframework.boot' version '2.6.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 = '1.8'
ext {
snippetsDir = file('build/generated-snippets')
}
bootJar {
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'BOOT-INF/classes/static/docs'
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'junit:junit:4.13.1'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation "org.mockito:mockito-core:2.24.0"
testImplementation group: 'org.mockito', name: 'mockito-all', version: '1.10.19'
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.7'
implementation group: 'commons-io', name: 'commons-io', version: '2.6'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
asciidoctor {
dependsOn test
inputs.dir snippetsDir
}
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")
}
test {
useJUnitPlatform()
}
build {
dependsOn copyDocument
}
Rest Docs 적용하기
restdocs를 적용하기 위해서는 2가지가 필요하다.
첫번째로 테스트 코드를 작성할 때 restdocs와 관련된 내용을 기입해야한다.
만드는 법은 상당히 단순하다.
먼저 어노테이션을 사용해야한다.
내가 사용한 어노테이션은
@AutoConfigureRestDocs
이걸 사용하면 우리가 restdocs를 위해 사용하는 bean이나 어노테이션들을 가져와 준다. 이에 대한 자세한 설명은
공식 문서를 참고해주길 바란다. 나는 이를 이용함으로써 어떻게 testcode를 만들어야하는지에 초점을 맞추어 글을 적어보려고한다.
이후에 코드에 다음과 같이 andDo()부분에 넣어주면된다.
아래와 같이 말이다.
this.mvc.perform(post(TEST_URL)
...
.andDo(document("/post", // (1)
preprocessRequest(prettyPrint()), // (2)
preprocessResponse(prettyPrint()), // (3)
requestFields( // (4)
fieldWithPath("todo").description("할 일")
),
responseFields( // (5)
fieldWithPath("id").description("사용자 id"), //
fieldWithPath("todo").description("할 일")
))
);;
(1)부분은 우리의 테스트 코드로 인해 생길 adoc의 경로를 설정해주는 부분이다. 우리가 앞써 gradle에서 설정한 snippetsDir의 하위 루트를 의미한다.
이렇게 말이다. 우리가 /post로 설정함으로써 post하위에 우리의 test code가 만들어낸 여러 adoc이 예쁘게 저장된다.
(2), (3) 우리가 문서에 나올 json은 html입장에서는 그냥 string이다. 따라서 우리가 브라우저에서 보는 예쁜 형식으로 Json을 보여주기 위해서는 다음과 같이 preprocess~ 를 넣어주어 해당 과정에서 생기는 json에 대한 처리를 prettyprint()로 명시해줘야한다. 다음은 이를 썼을 때와 안 썼을 때의 차이점을 왼쪽과 오른쪽으로 나타내어보았다.
(4)번은 request-fields.adoc을 만들어주는 역할을 한다. 우리가 만약 api의 요구조건에 request 부분에 json이 들어간다고 할 때 해당 json의 field가 어떤 역할을 하는지 decription을 통해 적어줄 수 있다. 그렇게 되면 다음과 같이 부분이 자동으로 만들어진다.
(5)번은 마지막으로 response 영역에 대한 설명이다.
되게 response는 json으로 오게 되는데 이 부분에 대한 설명을 넣어주는 곳이다. 다음 예시와 (4)부분을 같이 보면 이해가 될 것이다.
여기서 중요한 점은 response를 모두 적어줘야 한다는 것이다. 만약 우리가 id를 response fields에서 숨기고 싶다고 한다면
fieldWithPath("id").description("사용자 id").ignored()
를 해줘야한다.
마지막으로는 전체적인 template을 만들어줘야한다.
내가 만든 template은 매우 기본적이기 때문에 더 많은 요소를 첨가하고 싶은 분들은 아래의 링크를 통해 보다 자세한 문법을 공부 하는걸 추천 드린다.
https://narusas.github.io/2018/03/21/Asciidoc-basic.html
= Todo API
notification-api-docs
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]
== Todo Home
=== Todo 생성
==== Request Sample
include::{snippets}/post/http-request.adoc[]
==== Request Field
include::{snippets}/post/request-fields.adoc[]
==== Response Sample
include::{snippets}/post/http-request.adoc[]
==== Response Fields
include::{snippets}/post/response-fields.adoc[]
여기서 내가 이야기 하고 싶은 부분은 총 2개이다.
1. =
이는 섹션을 나누게 해주는 부분이다. 섹션은 총 5단계까지 표현이 가능하다. 섹션이 중요한 점은 Table of Contents의 소제목으로 쓰이기 때문이다.
2. include
두번째는 우리가 테스트 코드를 통해 만든 여러 adoc파일을 주입해주는 부분이다. 우리가 위의 테스트 코드를 만들고 다음과 같이
build를 누르게 되면 우리가 위에서 봤던 adoc파일이 만들어진다.
차례로 설명하자면
1. curl-request.adoc, http-request.adoc
우리가 요청을 보내는 방식에 대한 문서화된 파일이다.
2. http-response
우리가 받은 response를 문서화 시켜준 부분이다.
3. path-parameter.adoc
리소스가 지원하는 path 파라미터를 설명하는 테이블이 존재한다.
3. request-body, fields, response-body, fields
이들은 request의 body와 각 필드에 대한 설명(코드 내에서 만들어줬던 부분)이 만들어져있는 곳이다.
Rest Docs 보기
이제 이렇게 만든 rest docs를 보는 방법 3가지를 설명하겠다.
첫번째로 template을 만들때
Intellij를 사용하는 경우 adoc파일을 수정하다 보면 오른쪽에 이들이 html로 변환되었을 때 어떤 모습인지 보여주는 plugin을 지원한다. adoc을 수정하는 경우 옆에 자동으로 뜨기 때문에 설치해주면 된다. 안뜨는 분들은
https://plugins.jetbrains.com/plugin/7391-asciidoc
요걸 설치해주면 된다.
두번째로는 build해서 보는 방법이다.
단순히 그냥 src/main/resources/static.docs 밑에 가면 만들어진 html파일을 볼 수 있다.
마지막으로
프로젝트를 실행 시키고
http://localhost:8080/docs/{restDocs 이름}으로 들어가면 브라우저로 restdocs에 접근할 수 있다.
처음에는 이거 사용하는게 뭔가 되게 복잡했다. 이는 gradle에 대한 이해가 부족해서였던 거 같다. 역시 자기가 쓰는 라이브러리나 프레임워크는 기본적으로 돌아가는 걸 확실히 알고 사용하는게 중요하는걸 이번에도 배울 수 있었다.
참조
https://spring.io/projects/spring-restdocs
긴 글 읽어주셔서 감사합니다.
틀린 부분이 있으면 댓글을 달아주시면 감사하겠습니다.
📧 : may3210@g.skku.edu
'개발 > Spring' 카테고리의 다른 글
[Spring]JUnit에서 Async메소드의 비정상적인 종료 - 2편 (0) | 2022.02.15 |
---|---|
[Spring]JUnit에서 Async메소드의 비정상적인 종료 - 1편 (0) | 2022.02.10 |
[Spring] Spring과 Paging - [1] (0) | 2022.01.20 |
[Spring] Spring Security의 이해 (0) | 2022.01.19 |
[Spring] Spring 을 이용한 웹 서비스 구조 (0) | 2022.01.12 |