본문 바로가기
Java Spring/MVC

Spring Rest Docs 만들기

by GGShin 2022. 7. 19.

Spring에서 배운 내용 중에 현재로서 제가 제일 좋아하는 부분이 바로 Spring Rest Docs를 만드는 것입니다 호호

잘 정리된 API 문서를 보면 기분이 좋더라구요.

Spring Rest Docs는 자동으로 API 문서를 제작해주는 기능인데, 수기로 API 문서를 작성할 때 생길 수 있는 오류를 막아주기 때문에 굉장히 유용하다고 합니다! 한가지 Spring Rest Docs를 제작하기 위해 넘어야 할 산은 Controller의 method를 모두 pass해야만 한다는 점입니다. 이 역시 코드를 여러번 작성하다 보면 익숙해지겠죠? 

 

이 포스팅에서는 Spring Rest Docs를 만들기 위해 준비해야하는 것들과 문서에 포함할 내용을 어떻게 코드로 알려주면 되는지 알아보도록 하겠습니다.

 

Spring Rest Docs를 생성하는 흐름은 다음과 같습니다. 

 

Test code (Controller Slice Test)작성 -> Slice test task 실행 (-> API doc snippet (.adoc) 파일 생성) -> Snippet을 모아 전체 API doc 생성 (-> API doc을 HTML로 변환)  

*괄호 부분은 IDE에서 해주는 작업입니다. 

 

1. 먼저 기본 설정부터 해보도록 하겠습니다. gradle에 관련된 plugin, dependency 등을 추가하고 설정하는 사전 작업이 필요합니다.

plugins {
	...
    // (1) .adoc 확장자 파일인 AsciiDoc을 생성해주는 Asciidoctor 사용하기 위한 plugin 추가
	id "org.asciidoctor.jvm.convert" version "3.3.2"    
	...
}

...
// (2) ext 변수의 set() method로 API snippet 생성 경로 지정
ext {
	set('snippetsDir', file("build/generated-snippets"))
}

// (3) AsciiDoctor에서 사용되는 의존 그룹 지정
// (:asciidoctor task가 실행되면 asciidoctorExtensions라는 그룹을 지정함)
configurations {
	asciidoctorExtensions
}

dependencies {
    // (4) spring-restdocs-core, spring-restdocs-mockmvc dependency 추가
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
  
  // (5) spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리가 추가
  //(3)에서 지정한 asciidoctorExtensions 그룹에 포함 됨
	asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'

	...
}

// (6) :test task 실행 시 API 문서 생성 snippet directory path 설정
tasks.named('test') {
	outputs.dir snippetsDir
	useJUnitPlatform()
}

// (7) :asciidoctor task 실행 시 Asciidoctor 사용을 위해 asciidoctorExtensions을 설정
tasks.named('asciidoctor') {
	configurations "asciidoctorExtensions"
	inputs.dir snippetsDir
	dependsOn test
}

// (8) :build task 실행 전 실행되는 task
task copyDocument(type: Copy) {
	dependsOn asciidoctor //(8-1):copyDocumnet 실행 전에 asciidoctor task 먼저 시행되도록 설정
	from file("${asciidoctor.outputDir}") //(8-2)build/docs/asciidoc/ 경로에 생성되는 index.html을 copy함
	into file("src/main/resources/static/docs") //(8-3)지정된 경로로 index.html추가해 줌
}

build {
	dependsOn copyDocument //(9) :build 실행 전 먼저 실행되도록 설정
}

// (10) 애플리케이션 실행 파일이 생성하는 :bootJar task
bootJar {
	dependsOn copyDocument    // (10-1) :bootjar task 실행 전에 실행되도록 의존성 설정
	from ("${asciidoctor.outputDir}") {  // (10-2)Asciidoctor 실행 시 생성되는 index.html을 jar안에 추가해 줌
		into 'static/docs'     // (10-3)
	}
}

 

**참고

(8)의 src/main/resources/static/docs에서 copy된 index.html은 외부에 제공하기 위한 용도이고

(10)에서는 index.html을 application 실행 파일인 jar에 포함해 web browser에서 API문서를 확인하기 위한 용도입니다.

 

2. gradle 파일 설정을 마쳤으면 index.adoc이 생성될 경로를 만들어줍니다.

Gradle 기반으로 프로젝트를 할 경우에는 src/docs/asciidoc/ 디렉토리를 생성해두어야 합니다. Maven일 때는 src/main/asciidoc/ 디렉토리를 만들어주어야 합니다.이 경로가 설정되어 있지 않으면 index.adoc이 생성되지 않더라구요. 지난번에 디렉토리명에 오타를 냈더니 생성이 안되었습니다.  

 

3. Controller Test case 작성하기

지난번에 Controller test를 작성하는 방법에 대해 알아보았습니다. asciidoc에 어떤 내용을 담을 것인지만 명시하면 되기 때문에 , test가 잘 작성되어 있다면 큰 어려움이 없습니다. 

 

혹시 Controller test 작성에 절차가 궁금하시다면 아래 포스팅을 보고 오셔도 좋을 것 같습니다. 

https://ittingz.tistory.com/139

 

Mockito 적용하여 Controller test 해보기

Testing 시에 Layer간의 연결을 끊고 Layer 별 기능을 독립적으로 테스트할 수 있도록 도와주는 도구가 바로 Mockito 입니다.🍸 왜 layer별로 독립적인 테스트가 필요할까요? 특정 계층의 테스트만 필요

ittingz.tistory.com

 

 

코드를 한 번 살펴보도록 하겠습니다.

 

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs  // (1)
public class MemberControllerRestDocsTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper mapper;

    @Autowired
    private Gson gson;

    @Test
    public void postMemberTest() throws Exception {
   		//given
        MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
        String content = gson.toJson(post);

        MemberDto.response responseDto =
                new MemberDto.response(1L,
                        "hgd@gmail.com",
                        "홍길동",
                        "010-1234-5678",
                        Member.MemberStatus.MEMBER_ACTIVE,
                        new Stamp());

        given(mapper.memberPostToMember(Mockito.any(MemberDto.Post.class))).willReturn(new Member());

        given(memberService.createMember(Mockito.any(Member.class))).willReturn(new Member());

        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

        //when
        ResultActions actions =
                mockMvc.perform(
                        post("/v11/members")
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

        // then
        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(  // (2) 
                
                        "post-member",     // (2-1)
                        preprocessRequest(prettyPrint()),      // (2-2)
                        preprocessResponse(prettyPrint()),     // (2-3)
                        requestFields(   // (2-4)
                                List.of(
                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), // (2-5)
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호")
                                )
                        ),
                        responseFields(        // (2-6)
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"), // (2-7)
                                        fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
                                        fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("data.phone").type(JsonFieldType.STRING).description("휴대폰 번호"),
                                        fieldWithPath("data.memberStatus").type(JsonFieldType.STRING).description("회원 상태"),
                                        fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수")
                                )
                        )
                ));
    }
}

 

Mockito 사용법 포스팅에 나온 코드에 API Docs 관련 코드만 추가된 것입니다. 다른 부분은 따로 설명하지 않고 API Docs 관련 부분만 설명하도록 하겠습니다. 

 

  • (1) @AutoConfigureRestDocs 은 명칭만 봐도 RestDocs와 관련된 annotation이라는 것을 알 수 있습니다. 자동으로 Rest Docs를 configure해준다고 하네요. 우리에게 필요한 것이니 이 annotation을 붙여줍니다.

 

  • (2) 검증하는 then 부분에서 어떤 내용을 document에 넣을지 설정할 수 있습니다. andDo() method를 사용합니다. 
    •    (2-1)에서 설정한 이름은 API 문서 스니핏의 식별자 역할을 합니다. Test가 통과됐을 때 “post-member” 라는 directory가 생성되고, 하위에 문서 스니핏이 생성됩니다.
    • (2-2), (2-3) 문서 스니핏을 생성하기 전에 request와 response에 해당하는 문서 영역을 전처리하는 역할을 합니다.
    • (2-4)에서는 request 부분에 어떤 field들이 있는지를 명시해주면 됩니다. requestFields는 List형식을 parameter로 받기 때문에 List.of를 사용해주었습니다. 
      • (2-5)에 나온 코드를 하나씩 살펴보면,
        • fieldWithPath("email") : fieldWithPath()로 field명을 명시해줍니다.
        • .type(JsonFieldType.STRING) : field의 데이터 타입을 명시해줍니다.
        • .description("이메일") : 해당 필드를 문서에 어떻게 설명할 것인지 명시해줍니다.
    • (2-6)에서는 response에 어떤 field들이 있는지 명시해주면 됩니다. 세부사항은 requestFields를 작성할때와 동일합니다.

✅ field 데이터 타입을 명시할 때, 숫자의 경우는 int, long, double등을 모두 통틀어서 JsonFieldType.NUMBER로 작성하면 됩니다. 그리고 responseFields에 나온 "data" field와 같이 하나의 객체인 경우는 JsonFieldType.OBJECT로 작성합니다.

 

✅ client로부터 request를 받을 때 request body에만 정보가 담긴 것은 아닙니다. 어떨 때는 uri에 담기기도 하고, url 안에 parameter로 넘어오기도 합니다. 상황에 따라 어떤 method를 사용해야하는지 알아보겠습니다.

  • request body에 정보가 담겨있을 때: requestFields 사용 
    • 위의 예시에서 확인
  • url parameter에 정보가 담겨 있을 때: requestParameters 사용
//when
ResultActions actions =
       mockMvc.perform(
                        get("/members/")
                                .param("page", page)
                                .param("size", size)
       );

//then
      actions
             .andExpect(status().isOk())
             .andDo(document(
                        "get-members",
                        //...
                        requestParameters(
                                List.of(
                                        parameterWithName("page").description("페이지"),
                                        parameterWithName("size").description("사이즈")

                                )

                        ),
          //...
  • uri에 정보가 담겨 있을 때(Controller method 파라미터에 @PathVariable이 있을 때): pathParameters 사용
//when
ResultActions actions =
         mockMvc.perform(
                  get("/members/{member-id}", memberId)
         );

//then
        actions
                //...
                .andDo(document(
                        "get-member",
                        //... 
                        , pathParameters(
                        		//mockMvc 부분에 설정한 {member-id}에 해당
                                parameterWithName("member-id").description("회원 식별자")
                        ),
         //...

 

 

이렇게 설정을 해준 다음 application을 실행하면 build/generated-snippets/post-member 경로에 .adoc 파일들이 생성됩니다.

생성되는 .adoc 파일들을 snippet이라고 합니다.

 

Controller에 있는 method의 snippet이 모두 생성이 되면, snippet들을 하나의 html파일로 합쳐주어야 API Document가 완성됩니다. 생성된 snippet들을 어떻게 합치는지 다음번에 한 번 알아보도록 하겠습니다.

 

조언이 있으시면 댓글로 알려주세요.

감사합니다. :) 

 

 

 


참고 자료

https://jaehun2841.github.io/2019/08/04/2019-08-04-spring-rest-docs/#Test-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1

 

Spring Rest Docs를 이용한 API 문서 만들기 | Carrey`s 기술블로그

Spring Rest API 문서를 자동으로 생성하고자 할 때, 보통 Swagger로 많이 사용하지만 이번에는 Spring Rest Docs를 사용하여 API 문서를 자동으로 작성 할 수 있도록 해봤습니다. 포스팅에 작성된 코드는 htt

jaehun2841.github.io

https://techblog.woowahan.com/2597/

이 자료 좋음!

 

Spring Rest Docs 적용 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요? 우아한형제들에서 정산시스템을 개발하고 있는 이호진입니다. 지금부터 정산시스템 API 문서를 wiki 에서 Spring Rest Docs 로 전환한 이야기를 해보려고 합니다. 1. 전환하는

techblog.woowahan.com

https://velog.io/@sa833591/SpringBootTest-4#2-requestparameter

이 자료 좋음!

 

[SpringBootTest] Spring RestDocs 작성하기

들어가면서 👋테스트 코드를 작성하면 필수적(?)으로 Spring RestDocs를 스쳐지나보게 된다. Spring RestDocs를 사용하게 되면 우선 테스트는 거쳐진 코드로 생각할 수 있어 안정적인(?) 코드라 생각할

velog.io

 

반응형

'Java Spring > MVC' 카테고리의 다른 글

@Controller와 @RestController의 차이점  (3) 2022.08.06
em.flush() vs tx.commit()  (1) 2022.07.23
Mockito 적용하여 Controller test 해보기  (0) 2022.07.18
Entity mapping-Cascade의 역할  (0) 2022.07.17
[Spring] Spring JDBC 사용하기  (0) 2022.07.02