관리 메뉴

제뉴어리의 모든것

[Section 3] [Spring MVC] API 문서화 본문

코드스테이츠/정리 블로깅

[Section 3] [Spring MVC] API 문서화

제뉴어리맨 2022. 10. 18. 00:03

전체 항목

  • Spring Rest Docs란?
  • Spring Rest Docs 적용하기

Spring Rest Docs란?

작성된 Test의 내용을 토대로 API 문서를 자동으로 만들어 주는 Spring의 모듈 중 하나이다.

Spring Rest Docs 는 문서 작성 도구로 기본적으로 Asciidoctor 를 사용하며, 이것을 사용해 HTML 을 생성한다. 필요한 경우 Markdown 을 사용하도록 변경할 수 있다

 

https://www.slideshare.net/RomanTsypuk/test-driven-documentation-with-spring-rest-docs-76221038

위와 같은 흐름으로 진행된다.

  • 테스트 코드를 실행하여 스니핏(snippets) 이라는 조각 문서(.adoc)를 만든다
  • 스니핏을 기반으로 API 문서를 생성한다 (.adoc)
  • API 문서를 HTML 로 변환한다.

 

레퍼런스 사이트:

 

https://spring.io/projects/spring-restdocs#learn

https://docs.spring.io/spring-restdocs/docs/current/reference/html5/

https://docs.spring.io/spring-restdocs/docs/current/api/

 

Spring Rest Docs 적용하기

 

 

  • build.gradle 적용
plugins {
    id 'org.springframework.boot' version '2.7.4'
    id 'io.spring.dependency-management' version '1.0.14.RELEASE'
    id 'java'
    id "org.asciidoctor.jvm.convert" version "3.3.2" .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asciidoctor를 사용하기 위한 플러그인을 추가
}




group = 'kyh.toy'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

configurations {
    asciidoctorExt
}


dependencies {



    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    //Auditing
    implementation'org.springframework.boot:spring-boot-starter-web'
    implementation'org.projectlombok:lombok'
    implementation'org.springframework.boot:spring-boot-starter-data-jpa'

    //h2
    runtimeOnly 'com.h2database:h2'

    //유효성 어노테이션
    implementation 'org.springframework.boot:spring-boot-starter-validation'





    //Json 맵퍼
    implementation 'com.google.code.gson:gson:2.9.0'




    //rest docs
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'


    //MapStruct
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'



}

//rest docs
ext {
    snippetsDir = file('build/generated-snippets') //스니핏이 생성될 경로를 변수에 할당
}


tasks.named('test') { //spring boot test로 인해 원래 있는 test task
    outputs.dir snippetsDir //rest docs, test task 작동할때 생성되는 스니핏의 저장 경로를 snippetsDir 에 저장된 경로로 설정하겠다
    useJUnitPlatform()
}

asciidoctor { //실제 스니핏 asciidoc 문서를 만드는 task
    configurations 'asciidoctorExt' //asciidoctor 작의 구성으로 configurations 을 사용하겠다.
    inputs.dir snippetsDir //해당 작업의 input 디렉토리를 snippetsDir로 하겠다. 즉, test task의 결과물이 asciidoctor task의 인풋이다
    dependsOn test //test task가 먼저 시작되고 해당 task가 돌아간다 (즉, 의존하겠다)
}

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    println "asciidoctor output: ${asciidoctor.outputDir}"
    from file("${asciidoctor.outputDir}")
    into file("src/main/resources/static/docs")
}


bootJar {
    dependsOn copyDocument  //asciidoctor task가 먼저 시작되고 해당 task가 돌아간다 (즉, 의존하겠다)
    from ("${asciidoctor.outputDir}") { //asciidoctor의 결과물을 jar 파일 안에 static/docs 경로에 복사한다.
        into 'static/docs'
    }
}




build {
    dependsOn copyDocument
}

 

 

  • 실제 문서를 생성하는 Test 코드
package kyh.toy.wisesaying.member;


import com.google.gson.Gson;
import kyh.toy.wisesaying.ControllerTestHelper;
import kyh.toy.wisesaying.member.controller.MemberController;
import kyh.toy.wisesaying.member.dto.MemberDto;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.MediaType;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;

import java.util.List;

import static kyh.toy.wisesaying.utils.apiDocumentUtils.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


//@ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) //RestDocumentationExtension.class - 출력 디렉토리 자동 구성
@WebMvcTest(MemberController.class) // api 계층의 테스트에 필요한 빈들만 등록, 통합테스트 보다 가벼움
@MockBean(JpaMetamodelMappingContext.class) //Jpa 관련 빈들을 등록, 해당 애노테이션 없으면 현재 @EnableJpaAuditing 때문에 에러남. @EnableJpaAuditing이 @SpringBootApplication 와 같이 쓰여서 테스트시에도 활성화되는데, 그러면서 Jpa 관련 빈들을 찾기 때문.
@AutoConfigureRestDocs

public class MemberControllerRestDocsTests {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    Gson gson;

    String baseURI = "/api_v1/members";


    @Test
    void postMemberTest() throws Exception {


        //given
        MemberDto.Post post = new MemberDto.Post();
        post.setEmail("january@gmail.com");
        post.setName("멋쟁이");
        post.setPhone("010-2222-3333");
        post.setPassword("1234");


        String content = gson.toJson(post);


        //when
        // 단순히 request 보내는 부분
        ResultActions actions = mockMvc.perform(post(baseURI)
                .accept(MediaType.APPLICATION_JSON)
                .contentType(MediaType.APPLICATION_JSON)
                .content(content));
        //~ 단순히 request 보내는 부분


        //then
        MvcResult mvcResult = actions.andExpect(status().isCreated())
                .andExpect(jsonPath("$.data.email").value(post.getEmail()))
                .andExpect(jsonPath("$.data.name").value(post.getName()))
                .andExpect(jsonPath("$.data.phone").value(post.getPhone()))
                .andDo(document("post-member",  //실질적으로 문서를만드는 구문, document()
                        //request 관련 문서를 만들기전, response 과련 무서를 만들기 전 전처리기 추가
                        getRequestPreProcessor(), getResponsePreProcessor(),
                        requestFields( //문서에 입력되는 실제 필드 내용 정의
                                List.of(
                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
                                        fieldWithPath("password").type(JsonFieldType.STRING).description("비밀 번호")
                                )
                        ),
                        responseFields( //문서에 입력되는 실제 필드 내용 정의
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                        fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
                                        fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data.phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
                                        fieldWithPath("data.cardList").type(JsonFieldType.ARRAY).description("카드 리스트").ignored()
                                )
                        ))).andReturn();


        System.out.println(mvcResult.getResponse().getContentAsString());

    }

    @Test
    void patchMemberTest() {

    }

}