4 분 소요

클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 되는 요청 정보를 문서로 잘 정리하는 것



build.gradle 설정

Spring Rest Docs를 사용해서 API 문서를 생성하기 위해서는 .adoc 문서 스니핏을 생성해주는 Asciidoctor가 필요하다.

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id "org.asciidoctor.jvm.convert" version "3.3.2" // .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asiidoctor를 사용하기 위한 플러그인
	id 'java'
}

group = 'com.codestates'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

// ext 변수의 set() 메서드를 이용해서 API 문서 스니핏이 생성될 경로를 지정
ext {
	set('snippetsDir', file("build/generated-snippets"))
}

// AsciiDoctor에서 사용되는 의존 그룹을 지정함
configurations {
	asciidoctorExtensions
}

dependencies {
  // spring-restdocs-core, spring-restdocs-mockmvc 의존 라이브러리 추가됨
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
  
  // spring-restdocs-asciidoctor 의존 라이브러리 추가. asciidoctorExtensions 그룹에 포함됨
	asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'

	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	implementation 'org.mapstruct:mapstruct:1.5.1.Final'
	annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.1.Final'
	implementation 'org.springframework.boot:spring-boot-starter-mail'

	implementation 'com.google.code.gson:gson'
}

// :test task 실행 시, API문서 생성 스니핏 디렉토리 경로를 설정
tasks.named('test') {
	outputs.dir snippetsDir
	useJUnitPlatform()
}

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

// :build task 실행 전에 실행되는 task. :copyDocument task가 수행되면 index.html 파일이
// src/main/resources/static/docs에 copy 되며, copy된 html파일은 API문서를 파일 형태로 외부에 제공하기 위한 용도로 사용됨 (1)
task copyDocument(type: Copy) {
	dependsOn asciidoctor            // :asciidoctor task가 실행된 후에 task가 실행 되도록 의존성을 설정
	from file("${asciidoctor.outputDir}")   // build/docs/asciidoc/ 경로에 index.html을 추가해 줌
	into file("src/main/resources/static/docs")   // src/main/resources/static/docs 경로로 index.html을 추가해 줌
}

build {
	dependsOn copyDocument  // :build task가 실행되기 전에 :copyDocument task가 실행 되도록 의존성을 설정한다.
}

// 애플리케이션 실행 파일이 생성하는 :bootJar task 설정 (2)
bootJar {
	dependsOn copyDocument    // :bootJar task 실행 전에 :copyDocument task가 실행 되도록 의존성을 설정
	from ("${asciidoctor.outputDir}") {  // Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가해 줌 
		into 'static/docs'                 // jar 파일에 index.html을 추가해 줌으로써 웹 브라우저에서 접속 후, API 문서를 확인할 수 있음
	}
}

(1)에서 copy 되는 index.html은 외부에 제공하기 위한 용도, (2)에서는 index.html을 애플리케이션 실행 파일인 jar 파일에 포함해서 웹 브라우저에서 API 문서를 확인하기 위한 용도이다.

build.gradle 설정 다음으로 Gradle 기반 프로젝트에서는 src/docs/asciidoc/index.adco 를 생성한다.



MemberController 클래스에 대한 API 문서 생성용 테스트 케이스 작성

import com.codestates.member.controller.MemberController;
import com.codestates.member.mapper.MemberMapper;
import com.codestates.member.service.MemberService;
import com.google.gson.Gson;
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.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.test.web.servlet.MockMvc;

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

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper mapper;

    @Autowired
    private Gson gson;

    @Test
    public void getMemberTest() throws Exception {
        // TODO 여기에 MemberController의 getMember() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성
    }

    @Test
    public void getMembersTest() throws Exception {
        // TODO 여기에 MemberController의 getMembers() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성
    }

    @Test
    public void deleteMemberTest() throws Exception {
        // TODO 여기에 MemberController의 deleteMember() 핸들러 메서드 API 스펙 정보를 포함하는 테스트 케이스를 작성
    }
}


getMemberTest()

@Test
public void getMemberTest() throws Exception {
    Member member = new Member("abc@gmail.com","Demuu","010-1111-1111");
    member.setMemberId(1L);
    member.setMemberStatus(Member.MemberStatus.MEMBER_ACTIVE);
    long memberId = 1L;
    MemberDto.Response memberResponse = new MemberDto.Response(
            1L, "abc@gmail.com","Demuu","010-1111-1111", Member.MemberStatus.MEMBER_ACTIVE, new Stamp());

    given(memberService.findMember(Mockito.anyLong())).willReturn(member);
    given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(memberResponse);

    mockMvc.perform(get("/v11/members/{member-id}", memberId))
            .andExpectAll(
                    status().isOk(),
                    jsonPath("$.data.memberId").value(member.getMemberId()),
                    jsonPath("$.data.email").value(member.getEmail()),
                    jsonPath("$.data.name").value(member.getName()),
                    jsonPath("$.data.phone").value(member.getPhone()),
                    jsonPath("$.data.memberStatus").value(member.getMemberStatus().getStatus())
            ) .andDo(document(
                    "get-member",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    pathParameters(
                            parameterWithName("member-id").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.memberStatus").type(JsonFieldType.STRING).description("회원 상태"),
                    fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프")
            ))));
}


getMembersTest()

@Test
public void getMembersTest() throws Exception {
    List<Member> members = new ArrayList<>();
    members.add(new Member());
    members.add(new Member());
    members.add(new Member());
    members.add(new Member());
    members.add(new Member());
    members.add(new Member());

    MultiValueMap<String, String> info = new LinkedMultiValueMap<>();
    int page = 2; int size = 3;
    info.add("page", String.valueOf(page));
    info.add("size", String.valueOf(size));

    PageRequest pageRequest = PageRequest.of(page - 1, size);
    int start = size * (page - 1);
    int end = size * (page - 1) + size;
    Page<Member> pageMembers = new PageImpl<>(members.subList(start,end), pageRequest, members.size());
    List<MemberDto.Response> pageMemberResponseDto = new ArrayList<>();

    for(int i = 1; i <= pageMembers.getContent().size(); i++){
        pageMemberResponseDto.add(new MemberDto.Response(
                i, "abc"+i+"@gmail.com","Demuu"+i,"010-1111-111"+i, Member.MemberStatus.MEMBER_ACTIVE,new Stamp()));
    }

    given(memberService.findMembers(Mockito.anyInt(), Mockito.anyInt())).willReturn(pageMembers);
    given(mapper.membersToMemberResponses(Mockito.anyList())).willReturn(pageMemberResponseDto);

    mockMvc.perform(
            get("/v11/members")
                    .params(info)
                    .accept(MediaType.APPLICATION_JSON)
    ).andExpect(status().isOk())
            .andExpect(jsonPath("$.data.size()").value(size))
            .andDo(document(
                    "get-members",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    requestParameters(List.of(
                            parameterWithName("page").description("페이지 번호"),
                            parameterWithName("size").description("페이지 크기")
                    ))
            ));
}


deleteMemberTest()

@Test
public void deleteMemberTest() throws Exception {
    long memberId = 1L;
    doNothing().when(memberService).deleteMember(Mockito.anyLong());
    
    mockMvc.perform(
            delete("/v11/members/{member-id}", memberId)
                    .accept(MediaType.APPLICATION_JSON)
    ) .andExpect(status().isNoContent())
            .andDo(document(
                    "delete-member",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    pathParameters(
                            parameterWithName("member-id").description("회원 식별자")
                    )
            ));
}


docs/asciidoc/index.adoc

=== 특정 회원 찾기
.curl-request
include::{snippets}/get-member/curl-request.adoc[]

.http-request
include::{snippets}/get-member/http-request.adoc[]

.http-response
include::{snippets}/get-member/http-response.adoc[]

.httpie-request
include::{snippets}/get-member/httpie-request.adoc[]

.path=parameters
include::{snippets}/get-member/path-parameters.adoc[]

.request-body
include::{snippets}/get-member/request-body.adoc[]

.response-body
include::{snippets}/get-member/response-fields.adoc[]

.response-fields
include::{snippets}/get-member/response-fields.adoc[]

=== 모든 회원 찾기
.curl-request
include::{snippets}/get-members/curl-request.adoc[]

.http-request
include::{snippets}/get-members/http-request.adoc[]

.http-response
include::{snippets}/get-members/http-response.adoc[]

.httpie-request
include::{snippets}/get-members/httpie-request.adoc[]

.request-body
include::{snippets}/get-members/request-body.adoc[]

.request-fields
include::{snippets}/get-members/request-fields.adoc[]

.request-parameters
include::{snippets}/get-members/request-parameters.adoc[]

.response-body
include::{snippets}/get-members/response-body.adoc[]

=== 특정 회원 삭제
.curl-request
include::{snippets}/delete-member/curl-request.adoc[]

.http-request
include::{snippets}/delete-member/http-request.adoc[]

.http-response
include::{snippets}/delete-member/http-response.adoc[]

.httpie-request
include::{snippets}/delete-member/httpie-request.adoc[]

.path-parameters
include::{snippets}/delete-member/path-parameters.adoc[]

.request-body
include::{snippets}/delete-member/http-response.adoc[]

.response-body
include::{snippets}/delete-member/http-response.adoc[]

.response-fields
include::{snippets}/delete-member/http-response.adoc[]



추가 학습


Asciidoc란?

Spring Rest Docs를 통해 생성되는 텍스트 기반 문서 포맷.

Asciidoc은 주로 기술 문서 작성을 위해 설계된 가벼운 마크업 언어이다.

Asciidoc 포맷을 사용해서 메모, 문서, 기사, 서적, E-Book, 웹 페이지, 매뉴얼 페이지, 블로그 게시물 등을 작성할 수 있으며

Asciidoc 포맷으로 작성된 문서는 HTML, PDF, EPUB, 매뉴얼 페이지를 포함한 다양한 형식으로 변환될 수 있다.


Asiidoctor란?

AsciiDoc 포맷의 문서를 파싱해서 HTML5, 메뉴얼 페이지, PDF 및 EPUB3 등의 문서를 생성하는 툴.


카테고리: ,

업데이트:

댓글남기기