프로젝트/게시판 프로젝트

Spring REST Docs 적용하기(+ html 생성안됨 에러 해결, ./grdlew build 에러 해결, 각종에러해결)

Junuuu 2022. 6. 7. 01:11
반응형

지난 포스팅을 통해 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 파일들이 만들어집니다.

테스트 코드 실행 후 생성된 adoc 파일들

build.gradle에서  우리가 설정했던 경로로 들어간 것입니다.

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 명령어를 입력하니 파일들이 정상적으로 만들어졌습니다.

build/docs
resources/statis/docs

성공적으로 생성이 완료되었습니다

 

 

기능 고도화

현재는 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에는 반영되지 않았습니다.

local

음.. 뭔가 빌드 쪽에서 이상이 발생한 것 같습니다.

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이 가독성좋게 보입니다.

 

깔끔해진 모습

 

결론

공식문서를 참조하여 차근 차근 에러를 해결할 수 있다.

 

출처

https://docs.spring.io/spring-restdocs/docs/current/reference/html5/#getting-started-build-configuration

 

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

https://rok93.tistory.com/entry/Spring-Rest-Docs-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EC%82%AC%EC%9A%A9%EB%B2%95

 

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