이전 1탄 블로그 확인하기
2023.06.08 - [개발일지] - [솔로 프로젝트] TodoList Test 작성 및 API 문서화 하기 1탄
Spring REST Docs를 활용한 API 문서화
Spring REST Docs에 대해서는 1탄에 대해서 설명을 했다. 테스트 케이스가 성공해야하고 REST Docs 자체적으로 설정해놓은 코드가 실제 컨트롤러의 요청과 응답이랑 일치해야지 API 문서가 성공적으로 완성된다. Asciidoc 스니펫으로 여러 조각으로 만들어진 adoc 파일들을 index.adoc를 통해서 하나의 html 파일로 합치는 작업을 했다.
시작하기 전 짧게 리뷰를 하자면 에러 또 에러였다.... '이번에는 되겠지?' 했지만 돌아오는 답변은 '??? 어딜 어림도 없지'와 함께 여러 에러들을 내뿜어주었다. 아마 실질적으로 Spring REST Docs를 어떻게 써야하는지 보다는 에러 해결이 주 내용이라 생각된다.
Spring REST Docs를 사용하기 위한 초기 설정
공식사이트 : Spring REST Docs
위의 링크는 Spring REST Docs에 관한 공식문서이다. 무엇인가 사용 할 때는 공식문서를 보는게 좋다고 하고 공부할 때도 블로그 글들도 좋지만 꼭 공식문서를 확인하는 습관을 기르라고 하는데 아직은 모두 영어고(물론 한글로 번역하면 되지만) 용어나 이런 것들이 눈에 잘 들어오지 않아 어렵지만 그래도 믿을 만한 내용이고 궁금했던 것들의 대부분이 많이 들어있다.
import 해야 되는 내용들 build.gradle
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
configurations {
asciidoctorExtensions
}
dependencies {
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
println "asciidoctor output: ${asciidoctor.outputDir}"
from file("${asciidoctor.outputDir}")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
bootJar {
dependsOn copyDocument
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
build.gradle 에 의존성이나 필요한 설정들을 추가했다. 테스트를 시작한 후에 asciidocor를 실행한다던지 그렇게 해서 생성된 문서는 어느 폴더에 위치시켜야 한다던지 하는 내용들이 담겨 있다. 나머지 내용들은 공식문서를 확인해보면 시작하는 방법에 자세히 어떤 의미인지 설명되어 있다.
기존의 todoPost() 메소드에 API 문서화를 위한 코드 추가
@Test
void todoPost() throws Exception {
// given
TodoPostDto postTodo = new TodoPostDto("운동하기", 1, false);
TodoResponseDto response = new TodoResponseDto(1L, "운동하기", 1, false);
given(todoService.createTodo(Mockito.any(Todos.class))).willReturn(new Todos());
given(mapper.todoPostDtoToTodos(Mockito.any(TodoPostDto.class))).willReturn(new Todos());
given(mapper.todosToTodoResponseDto(Mockito.any(Todos.class))).willReturn(response);
String content = gson.toJson(postTodo);
// when
ResultActions actions =
mockMvc.perform(
post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON)
);
// then
actions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value(postTodo.getTitle()))
.andExpect(jsonPath("$.todoOrder").value(postTodo.getTodoOrder()))
.andExpect(jsonPath("$.completed").value(postTodo.isCompleted()))
.andDo( //✨ 여기서 부터 새로 추가된 내용!!!
document(
"post-Todo",
getRequestPreProcessor(),
getResponsePreProcessor(),
pathParameters(
parameterWithName("todo-id").description("Todo 식별자")
),
responseFields(
List.of(
fieldWithPath("title").type(JsonFieldType.STRING).description("할일 내용"),
fieldWithPath("todoOrder").type(JsonFieldType.NUMBER).description("할일 우선순위"),
fieldWithPath("completed").type(JsonFieldType.BOOLEAN).description("할일 완료 여부 (완료: true, 미완료: false)")
)
)
)
);
}
반복해서 사용하기 때문에 static으로 import 해와서 사용한 메소드가 많이 있다. 아래의 설명에서는 어떤 클래스의 메소드인지 간략하게 적어놓도록 하겠다.
- MockMvcRestDocumentation.document() : 도큐먼트 생성을 위한 실질적인 메소드이다. 해당 파라미터 안에 도큐먼트의 이름과 전처리, 후처리를 통해 예쁘게 어떻게 꾸밀지, 어떤 pathParameters를 가지는지 그리고 requestFiedls 또는 responseFields를 지정하면 된다.
- getRequestPreProcessor()와 getResponsePreProcessor() : 도큐먼트의 전처리 후처리를 손쉽게 사용하기 위해서 만들어놓은 인터페이스의 메소드이다. OperationRequestPreprocessor 객체를 반환하도록 만들었다.
- RequestDocumentation.pathParameters() : 해당 메소드의 인자로 파라미터의 이름과 설명을 같이 전달해주면 컨트롤러에 실제로 해당 파라미터가 존재하는지 확인해서 존재한다면 API문서로 만든다.
- RequestDocumentation.parameterWithName() : 인자로 파라미터의 이름을 지정한다. 추가로 .description 메소드를 통해 파라미터에 대한 설명도 추가 할 수 있다.
- PayloadDocumentation.responseFields : 요청을 컨트롤러에게 했을 때 어떤 응답을 받는지에 대해서 지정하고 API 문서화를 할 수 있다.
첫번째 에러, RestDocumentationRequestBuilders를 사용하지 않았습니다.
두둥탁! 당연히 에러가 뜨겠지 했지만 그래도 첫트에 성공하는 설레는 마음이 있었지만 역시나 에러가 떳다.
오류의 이유
// when
ResultActions actions =
mockMvc.perform(
post("/")
.contentType(MediaType.APPLICATION_JSON)
.content(content)
.accept(MediaType.APPLICATION_JSON)
);
여기서 post("/") 이 부분이 아래처럼 MockMvcRequestBuilders 의 메소드이다.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
오류 메세지를 보면 RestDocumentationRequestBuilders 를 사용하고 있냐고 물어보고 있다. 즉 RestDocumentation을 하기 위해서는 이 분을 바꿔줘야 할 필요가 있는 것 같다 생각하고 공식문서를 확인해보았다. 공식문서를 찾아보니 아래와 같이 나와 있다.
<공식문서 내용>
If you use MockMvc, to make the path parameters available for documentation, you must build the request by using one of the methods on RestDocumentationRequestBuilders rather than MockMvcRequestBuilders.
MockMvc를 사용해서 테스트를 할 때 문서화가 가능하도록 하고 싶다면 MockMvcRequestBuilders 대신에 RestDocumentationRequestBuilders를 쓰라고 아주 친절히 잘 설명되어 있다. 이 팁에 나와있는 것 말고는 또 다른 내용은 없는데 더욱 자세히 알려면 코드를 뜯어보거나 해야 할 것 같다.
아래처럼 코드를 수정했다.
이제는 잘 되겠지 ??? 😀✨
??? : 는 무슨… 어림도 없지!!!!
두번째 에러 : Path Parameters에 [todo-id] 가 없습니다.
늘 에러를 마주 할 때의 생각이지만 될 거라 생각했을 때 만나는 에러는 사람을 굉장히 당황하게 만든다 머릿속에 ‘아.. 또 뭐지 ??? 😫 하아..’ 하는 생각을 주지만 당화하지 않고 메세지를 잘 읽어보면 또 의외로 쉽게 풀리는 경우가 많다.
에러메세지 : Path parameters with the following name were not found in the request: [todo-id]
path parameters에 todo-id라는 것을 찾을수가 없다. 즉 postMapping에 todo-id를 입력받는게 없는데? 라고 이야기하는 것이다. 다시 한번 컨트롤러 코드랑 작성한 API 문서화를 위한 코드를 대조해보니 답을 찾았다.
.andDo(
document(
"post-Todo",
getRequestPreProcessor(),
getResponsePreProcessor(),
pathParameters( //✨ 이부분이 문제다!
parameterWithName("todo-id").description("Todo 식별자")
),
responseFields(
List.of(
fieldWithPath("title").type(JsonFieldType.STRING).description("할일 내용"),
fieldWithPath("todoOrder").type(JsonFieldType.NUMBER).description("할일 우선순위"),
fieldWithPath("completed").type(JsonFieldType.BOOLEAN).description("할일 완료 여부 (완료: true, 미완료: false)")
)
)
)
컨트롤러의 Post 부분
@PostMapping("/")
public ResponseEntity todoPost(@RequestBody TodoPostDto todoPostDto) {
// 비지니스 로직
Todos savedTodo = todoService.createTodo(mapper.todoPostDtoToTodos(todoPostDto));
return new ResponseEntity(mapper.todosToTodoResponseDto(savedTodo), HttpStatus.CREATED);
}
여기서 보면 todo-id를 받지 않는다.. 왜 나는 내가 설계해놓고 todo-id를 적어놓았을까?? 뭔가에 홀렸나보다. pathParameters에 대한 내용을 삭제했다.
적을까 말까하다가 아마 이런 부분이 Spring REST Docs의 장점이라 생각하여 마지막 오류를 적는다. 위의 내용을 삭제후 테스트를 돌렸는데 또 에러가 나왔다. 사실 이렇게 컴파일 단계에서 에러가 나와주는 것은 개발자에게 굉장히 친절한 것이다. 왜냐면 런타임 에러를 줄일 수 있기 때문에 진짜 문제는 런타임에서 나도 모르는 에러가 터지는거지 컴파일 단계의 에러는 언제든지 수정 할 수 있다.
이번엔 성공일까? 😂 역시나 ...
세번째 에러 : 바디에 너가 지정하지 않은 값이 또 있는데???
에러메세지 : The following parts of the payload were not documented: { "id" : 1 } org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented: { "id" : 1 }
결과값으로 전달하는 값이 있는데 내가 documentation 에 body로 응답받는 id 에 대한 내용을 기재하지 않아 발생한 에러이다.
responseFields(
List.of(
// 빠진 내용 : fieldWithPath("id").type(JsonFieldType.STRING).description("등록된 할일의 식별자"),
fieldWithPath("title").type(JsonFieldType.STRING).description("할일 내용"),
fieldWithPath("todoOrder").type(JsonFieldType.NUMBER).description("할일 우선순위"),
fieldWithPath("completed").type(JsonFieldType.BOOLEAN).description("할일 완료 여부 (완료: true, 미완료: false)")
)
)
위의 코드에 보면 응답 받는 response의 id 에 대한 내용이 빠져있어서 에러가 났다. 이처럼 Spring REST Docs는 내가 누락할 수 있는 내용들을 확인해주고 모든 테스트를 통과해야지만 API문서가 만들어진다. 에러를 해결해나가는 과정을 보면서 Spring REST Docs가 어떤지 더 잘 파악하게 되지 않을까 생각된다.
과연 이제는 마지막 수정일 될까?
네번째 에러 : 필드의 타입이 잘못 되었어!!!
또 에러가 났다… 위의 코드에 보면 fieldWithPath("id").type(JsonFieldType.STRING) 이부분에서 JsonFieldType을 STRING이 아니라 id는 숫자이기 때문에 NUMBER를 해야하는데 STRING이라고 에러가 났다.
이것을 통해서 알 수 있는 것은 Spring REST Docs는 내가 지정한 타입이 실제 타입과 일치하는지도 검사한다. 이런 과정을 통해서 API문서화가 된다면 굉장히 신뢰할만한 문서라 생각이 된다.
드디어!!!! 성공 !!! ✨
드디어…. 드디어 성공했다 ✨😀 마음이 편안해지는 저 초록색 체크박스 ㅎㅎ 드디어 보았다. 이제는 API Docs가 잘 만들었는지 확인해보면 아래와 같이 build 디렉토리 안에 잘 생성이 되었다.
하나하나 마다 request, response의 상세내역들이 들어가있다. 이 부분을 Asccidoctor를 통해서 예쁜 html파일로 통합하면 된다.
마지막 작업! : Index.adoc 설정
= To do 애플리케이션
:sectnums:
:toc: left
:toclevels: 4
:toc-title: Table of Contents
:source-highlighter: prettify
Lee Kyung Ju <wwmix@naver.com>
v1.0.0, 2023.06.09
***
== TodoController
=== 할일 등록
.curl-request
include::{snippets}/post-todo/curl-request.adoc[]
.http-request
include::{snippets}/post-todo/http-request.adoc[]
.request-fields
include::{snippets}/post-todo/request-fields.adoc[]
.http-response
include::{snippets}/post-todo/http-response.adoc[]
.response-fields
include::{snippets}/post-todo/response-fields.adoc[]
=== 할일 단일 조회
include::{snippets}/get-todo/curl-request.adoc[]
.http-request
include::{snippets}/get-todo/http-request.adoc[]
.path-parameters
include::{snippets}/get-todo/path-parameters.adoc[]
.http-response
include::{snippets}/get-todo/http-response.adoc[]
...
Index.adoc 에 예쁘게 어떤 내용이 담겨야하는지 설정을 해주면 된다. 그러면 경로에 있는 파일들을 불러와서 하나의 html 파일로 API문서화를 진행해준다 아마 asciidoc을 어떻게 써야하는지 또 스타일링이 궁금한 분이라면 아래에 링크를 첨부해두겠다.
드디어 이제 완성된 API문서를 볼 수 있다. 사실 다른 메소드를 테스트하고 문서화 하다가 발견된 에러가 아주 많다. 그 중 하나는 따로 떼어 내어서 블로그 글로 쓰려고 한다. 이러한 기록을 남겨두는게 사실 굉장히 귀찮은데 그래도 할 때마다 바로바로 노션에 남겨두니 이렇게 블로그로도 작성하기도 괜찮고 비슷한 에러를 만난 사람들에게 힌트가 되길 바란다.
결과
'개발일지' 카테고리의 다른 글
[pre-project 회고] 경험치를 올려 레벨업을 했다 feat.코드스테이츠 (0) | 2023.06.30 |
---|---|
[프리프로젝트] 프론트, 백엔드 서버 배포 관련 조사 (0) | 2023.06.29 |
[KPT] Section 4을 마치며 하는 회고 feat.코드스테이츠 백엔드 (1) | 2023.06.08 |
[솔로 프로젝트] TodoList Test 작성 및 API 문서화 하기 1탄 (0) | 2023.06.08 |
[리팩토링] 투두리스트 다크모드 코드 개선하기 feat. react (1) | 2023.06.06 |