Spring REST Docs 적용하기(+ html 생성안됨 에러 해결, ./grdlew build 에러 해결, 각종에러해결)
지난 포스팅을 통해 API 문서화를 위해 Swagger와 Spring Rest Docs를 비교하여 보았습니다.
그리고 분석을 통하여 좀 더 깔끔 명료한 문서를 만들 수 있고 문서와 코드의 괴리감이 생길 수 없는 Spring Rest Docs를 적용해보고자 합니다.
추가적으로 공부하다가 알게 된 점으로는 Postman으로도 API 문서를 만들 수 있습니다.
하지만 테스트 코드를 강제화 할 수 있는 Spring REST Docs를 사용하겠습니다.
공식문서와 다른 분들이 적용한 예시를 보고 포스팅을 작성해보겠습니다.
Swagger vs Spring REST Docs
https://junuuu.tistory.com/318?category=997278
API 문서화를 위한 Swagger와 Spring Rest Docs 비교
Swagger란? API 문서를 자동으로 만들어주는 라이브러리입니다. REST API를 편리하게 문서화해주고, 이를 통해 편리하게 API를 호출해보고 테스트할 수 있는 프로젝트입니다. 이를 활용하여 협업하는
junuuu.tistory.com
Spring REST Docs란?
Restful 서비스에 대한 정확하고 읽기 쉬운 문서를 자동으로 생성하도록 도와주는 라이브러리
Asciidoctor를 이용하여 HTML을 생성
각 테스트에서 스니펫이라고 하는 문서 조각을 만들어 주는데 이를 모아서 하나의 문서로 꾸밈
요구조건으로 테스트 케이스가 반드시 필요함
API Spec과 문서화를 위한 테스트 코드가 일치하지 않으면 테스트 빌드는 실패하게 됨
즉, 테스트 코드로 검증된 문서 (실제 코드에 추가되는 코드가 없는 장점)
Spring REST Docs 최소 요구사항
Java 8
Spring Framework 5 ( 5.0.2 or later)
빌드를 위한 build.gradle 의존 설정
바뀐 정보가 있을 수 있으니 여기 링크를 눌러 공식문서로 이동하여 참고하시면 좋을 것 같습니다.
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
configurations {
asciidoctorExt
}
dependencies {
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn test
}
주의할 점은 공식문서를 그대로 받아오셨다면 :{project-version}는 제거해주셔야 합니다.
다음 설정을 추가하면 스프링 부트에 의해 정적 파일이 제공됩니다.
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
jar가 빌드되기 전에 문서가 생성되고 생성된 문서는 jar에 포함됩니다.
JUnit 5 Test를 위한 세팅
@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
public class JUnit5ExampleTests {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext context;
@Before
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
.apply(documentationConfiguration(this.restDocumentation))
.build();
}
}
너무 길다고요?
Junit5 기준으로 Spring Boot를 사용하지 않으면 위와 같은 설정을 해주어야 합니다.
하지만 Spring Boot를 사용하면 어노테이션 한 개만 달아주면 됩니다.
@AutoConfigureRestDocs
문서화를 시키기 위해서는 테스트 코드에. andDo(document("이름"))을 달아주면 됩니다.
간단하게 중복 테스트 코드를 다음과 같이 변경하였습니다.
package anthill.Anthill.controller;
import anthill.Anthill.service.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@AutoConfigureRestDocs
@WebMvcTest(DuplicateController.class)
class DuplicateControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private MemberService memberService;
@Test
@DisplayName("닉네임 중복 테스트 false 반환")
public void checkNicknameDuplicateFalseTest() throws Exception {
//given
String response = "false";
boolean test = false;
String nickName = "test";
given(memberService.checkNicknameDuplicate(nickName)).willReturn(test);
//when
ResultActions resultActions = mvc.perform(get("/user-nickname/" + nickName));
//then
resultActions
.andExpect(status().isOk())
.andExpect(content().string(response))
.andDo(document("nick-name-non-duplicate"
));
}
@Test
@DisplayName("닉네임 중복 테스트 true 반환")
public void checkNicknameDuplicateTrueTest() throws Exception {
//given
String response = "true";
boolean test = true;
String nickName = "test";
given(memberService.checkNicknameDuplicate(nickName)).willReturn(test);
//when
ResultActions resultActions = mvc.perform(get("/user-nickname/" + nickName));
//then
resultActions
.andExpect(status().isOk())
.andExpect(content().string(response))
.andDo(document("nick-name-duplicate"));
}
}
이후에 테스트를 실행시키면 build -> generated-snippets에 adoc 파일들이 만들어집니다.
build.gradle에서 우리가 설정했던 경로로 들어간 것입니다.
Setting -> Plugins -> AsciiDoc 다운로드
adoc 파일 작성의 편의를 위해 AsciiDoc 플러그인을 설치합니다.
이제 생성된 스니펫들을 활용하겠습니다.
.adoc 파일을 생성해야 합니다. Gradle의 경우 src/docs/asciidoc/*. adoc 경로로 생성하면 됩니다.
저 같은 경우에는 api-doc.adoc 대신에 index.adoc으로 다음과 같이 작성했습니다
= REST Docs 문서 만들기 (글의 제목)
부제목(부제)
:doctype: book
:icons: font
:source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용
:toc: left // toc (Table Of Contents)를 문서의 좌측에 두기
:toclevels: 2
:sectlinks:
[[Member-API]]
== Member API
[[Member-중복-조회]]
=== Member 중복 조회
operation::nick-name-duplicate[snippets='http-request,path-parameters,http-response,response-fields']
operation::nick-name-non-duplicate[snippets='http-request,path-parameters,http-response,response-fields']
여기에 nick-name-duplicate는. andDo()에서 생성한 문서의 이름을 넣어주시면 됩니다.
만약 위에서 플러그인을 설치했다면 로컬에서 바로 문서가 어떻게 이루어지는지 확인할 수 있습니다.
하지만 현재 html 파일이 생성되지 않습니다.
인텔리제이 하단 상태바를 보면 terminal이 존재합니다.
여기서 Terminal로 이동하여 빌드를 위해./gradlew build 명령어를 입력했습니다.
저는 이때 Java compile 에러가 계속해서 발생하였습니다
이때 자바 설정도 올바르게 했는데 뭔가 이상하다 싶으시면 java -version 명령어를 입력하여 환경변수로 등록된 자바 버전을 확인해야 합니다.
저 같은 경우에는 프로젝트 JAVA 16 버전을 사용하였지만 환경변수에는 JAVA1.8버전이 등록되어 있어서 계속 에러가 발생했습니다.
이를 해결하고 난 뒤 ./gradlew build 명령어를 입력하니 파일들이 정상적으로 만들어졌습니다.
성공적으로 생성이 완료되었습니다
기능 고도화
현재는 document의 이름만 주었습니다.
좀 더 기능을 고도화 하는 방법에 대해 알아보겠습니다.
인자에 대한 설명과 결과를 설명하기 위해 다음과 같은 코드를 추가했습니다.
pathParameters(
parameterWithName("nickname").description("멤버 닉네임")
),
responseFields(
fieldWithPath("result").description("결과")
)
테스트가 제대로 수행되지 않고 IllegalArgumentException이 발생합니다.
java.lang.IllegalArgumentException: urlTemplate not found. If you are using MockMvc did you use RestDocumentationRequestBuilders to build the request?
코드를 다음과 같이 수정했습니다.
ResultActions resultActions = mvc.perform(RestDocumentationRequestBuilders.get("/user-nickname/" + nickName));
이번에는 새로운 에러가 발생합니다.
org.springframework.restdocs.snippet.SnippetException: Path parameters with the following names were not found in the request: [nickname]
음 Path Parameters로 nickname이라는 request를 찾을 수 없다고 합니다.
경로를 인식하지 못하는건가? 라는 생각이 들었습니다.
"user-nickname/" + nickName 으로 되어있던 부분을 "user-nickname/{nickname}, nickName 으로 변경해보았습니다.
ResultActions resultActions = mvc.perform(RestDocumentationRequestBuilders.get("/user-nickname/{nickname}",nickName));
새로운 에러입니다..
java.lang.ClassCastException: class java.lang.Boolean cannot be cast to class java.util.List (java.lang.Boolean and java.util.List are in module java.base of loader 'bootstrap')
뭔가 반환값이 Boolean으로 들어오는데 List형식으로 변환할 수 없다는 에러가 나왔습니다.
우선 어디가 문제일까 하고 다음 부분을 주석처리했습니다.
/*
,
responseFields(
fieldWithPath("result").description("결과")
)
*/
이제 테스트는 에러 없이 동작합니다.
중간점검으로 PathParameters가 잘 반영되었는지 확인해보겠습니다.
다시 terminal을 열고 ./gradlew build를 입력합니다.
local에는 잘 적용되었는데 localhost:8080/docs/index.html에는 반영되지 않았습니다.
음.. 뭔가 빌드 쪽에서 이상이 발생한 것 같습니다.
src/docs/asciidoc/index.adoc로 가서 저장을 다시 하고 build를 다시 수행하니까 잘 반영되었습니다.
이제 Path parameters까지 되었습니다.
에러가 발생했던 Response filed를 다시 보겠습니다.
주의할 점!
컨트롤러에서 객체를 반환하지 않는데 responseFields를 사용한다면 ClassCastException 예외가 발생할 수 있다.
추가로 컨트롤러에서 String 객체를 반환하면 content as it could not be parsed as JSON or XML 오류가 발생한다.
두 경우 모두 일반적인 객체를 반환하도록 설정해주어야 한다.
즉, 현재 String, Boolean을 반환하기 때문에 발생하는 에러 같습니다.
이를 해결하기 위해 MemberDuplicateResponseDTO를 생성했습니다.
package anthill.Anthill.dto.member;
import lombok.Builder;
import lombok.Getter;
@Getter
public class MemberDuplicateResponseDTO {
private String message;
@Builder
public MemberDuplicateResponseDTO(String message) {
this.message = message;
}
}
그리고 반환된 boolean을 포장하여 객체화시켰습니다.
public ResponseEntity<MemberDuplicateResponseDTO> checkNicknameDuplicate(@PathVariable String nickname) {
return ResponseEntity.ok(MemberDuplicateResponseDTO.builder()
.message(String.valueOf(memberService.checkNicknameDuplicate(nickname)))
.build());
이제 테스트가 정상적으로 잘 수행됩니다.
수많은 삽질 후 드디어 깔끔한 문서 하나가 완성되었습니다
위의 예시들은 Get 요청에 대해서만 나타냈습니다.
POST요청에 대해서도 한번 문서화를 해보겠습니다.
회원가입과 로그인을 수행하는 MemberController에 대해 문서화를 진행해보겠습니다.
회원가입 테스트코드
@Test
@DisplayName("회원 중복 발생안됬을 시 201 상태코드 반환")
public void memberNonDuplicateTest() throws Exception {
//given
MemberRequestDTO memberRequestDTO = getMemberRequestDTO();
String body = (new ObjectMapper()).writeValueAsString(memberRequestDTO);
boolean duplicateResult = false;
given(memberService.validateIsDuplicate(any())).willReturn(duplicateResult);
//when
ResultActions resultActions = mvc.perform(post("/members")
.content(body)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
);
//then
resultActions
.andExpect(status().isCreated())
.andDo(document("member-join-success",
requestFields(
fieldWithPath("userId").description("아이디"),
fieldWithPath("name").description("이름"),
fieldWithPath("nickName").description("닉네임"),
fieldWithPath("password").description("비밀번호"),
fieldWithPath("phoneNumber").description("전화 번호"),
fieldWithPath("address").description("주소")
)
));
}
index.adoc에서 operation은 이제 잘 추가하실 수 있을거라 믿고 더 이상 설명하지 않겠습니다.
조금 아쉬운점은 요청하는 JSON이 가독성이 너무 떨어집니다.
.andDo(document("member-join-success",
preprocessRequest(prettyPrint()),
requestFields(
fieldWithPath("userId").description("아이디"),
fieldWithPath("name").description("이름"),
fieldWithPath("nickName").description("닉네임"),
fieldWithPath("password").description("비밀번호"),
fieldWithPath("phoneNumber").description("전화 번호"),
fieldWithPath("address").description("주소")
)
));
위의 코드에서 preprocessRequest(prettyPrint())가 추가되었습니다.
이렇게하면 json이 가독성좋게 보입니다.
깔끔해진 모습
결론
공식문서를 참조하여 차근 차근 에러를 해결할 수 있다.
출처
Spring REST Docs
Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test, WebTestClient, or REST Assured.
docs.spring.io
https://backtony.github.io/spring/2021-10-15-spring-test-3/
Spring REST Docs 적용 및 최적화 하기
Java, JPA, Spring을 주로 다루고 공유합니다.
backtony.github.io
https://me-analyzingdata.tistory.com/entry/Rest-Docs-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
[Spring] Rest Docs 빌드 부터 사용까지
우리가 Api를 개발하고 이에 대한 스펙을 다른이에게 공유하기에 앞써 우리는 Api문서라는 것을 만들어야한다. Api 문서를 만드는 데에는 직접 markdown을 작성하거나 postman을 이용하거나 등등의
me-analyzingdata.tistory.com
Spring Rest Docs 소개 및 사용법
Spring Rest Docs 소개 Spring Rest Docs 공식 레퍼런스 Spring MVC test를 사용해서 문서의 일부분을 생성해낼 때 유용한 기능을 제공해주는 라이브러리이다. 우리가 만든 테스트를 실행할 때 사용하는 요청
rok93.tistory.com
https://tecoble.techcourse.co.kr/post/2020-08-18-spring-rest-docs/
API 문서 자동화 - Spring REST Docs 팔아보겠습니다
프로덕션 코드와 분리하여 문서 자동화를 하고 싶다고요? 신뢰도 높은 API 문서를 만들고 싶다고요? 테스트가 성공해야 문서를 만들 수 있다!! Spring REST Docs가 있습니다. API 문서를 자동화 도구로
tecoble.techcourse.co.kr
https://velog.io/@tmdgh0221/Spring-Rest-Docs-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0
Spring Rest Docs 적용해보기
API 문서 자동화, Spring Rest Docs
velog.io