ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring REST Docs 적용하기(+ html 생성안됨 에러 해결, ./grdlew build 에러 해결, 각종에러해결)
    프로젝트/게시판 프로젝트 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

     

    댓글

Designed by Tistory.