API 문서화 - Spring Rest Docs

jungseo·2023년 7월 4일
0

Spring

목록 보기
17/23
post-thumbnail

API 문서화란

API 문서화란 클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 되는 요청 정보(요청 URL(또는 URI), request body, query parameter 등)를 문서화 하는 것

API 사용을 위한 어떤 정보가 담겨 있는 문서를 API 문서 또는 API 스펙(사양, Specification)이라고 함

Spring Rest Docs

  • Swagger
    • 기능 구현 코드에 애너테이션을 추가하여 문서화
    • API 문서에서 바로 HTTP 요청 전송 가능
  • Spring Rest Docs
    • 테스트 코드에 문서화를 위한 코드 추가
    • 테스트 성공 시 API 문서 생성
    • 테스트 케이스를 일일이 작성해야 함

1. API 문서 생성 흐름

  1. 슬라이스 테스트 코드 작성
  2. API 스펙 정보 코드 작성
  3. test task 실행 (테스트 코드 실행 / build task(
  4. 테스트 성공시 API 문서 스니펫(.doc 파일) 생성

    스니펫(snippet) : 문서의 일부 조각
    테스트 케이스 하나당 하나의 스니펫이 생성되며, 여러 개의 스니펫을 모아서 하나의 API 문서를 생성

  5. API 문서 생성
  6. API 문서를 HTML로 변환

2. Spring Rest Docs 설정

1) build.gradle 설정
2) API 문서 스니펫을 사용하기 위한 템플릿 API 문서 생성

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"    // (1)
	id 'java'
}

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

repositories {
	mavenCentral()
}

// (2)
ext {
	set('snippetsDir', file("build/generated-snippets"))
}

// (3)
configurations {
	asciidoctorExtensions
}

dependencies {
        // (4)
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
  
        // (5) 
	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'
}

// (6)
tasks.named('test') {
	outputs.dir snippetsDir
	useJUnitPlatform()
}

// (7)
tasks.named('asciidoctor') {
	configurations "asciidoctorExtensions"
	inputs.dir snippetsDir
	dependsOn test
}

// (8)
task copyDocument(type: Copy) {
	dependsOn asciidoctor            // (8-1)
	from file("${asciidoctor.outputDir}")   // (8-2)
	into file("src/main/resources/static/docs")   // (8-3)
}

build {
	dependsOn copyDocument  // (9)
}

// (10)
bootJar {
	dependsOn copyDocument    // (10-1)
	from ("${asciidoctor.outputDir}") {  // (10-2)
		into 'static/docs'     // (10-3)
	}
}
  • (1) .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해 주는 Asciidoctor를 사용하기 위한 플러그인을 추가

  • (2) ext 변수의 set() 메서드를 이용해서 API 문서 스니펫이 생성될 경로를 지정

  • (3) AsciiDoctor에서 사용되는 의존 그룹을 지정, :asciidoctor task가 실행되면 내부적으로 (3)에서 지정한 ‘asciidoctorExtensions’라는 그룹을 지정

  • (4) 'org.springframework.restdocs:spring-restdocs-mockmvc' 의존 라이브러리가 추가

  • (5) spring-restdocs-asciidoctor 의존 라이브러리를 추가, (3)에서 지정한 asciidoctorExtensions 그룹에 의존 라이브러리가 포함

  • (6) :test task 실행 시, API 문서 생성 스니펫 디렉토리 경로를 설정

  • (7) :asciidoctor task 실행 시, Asciidoctor 기능을 사용하기 위해 :asciidoctor task에 asciidoctorExtensions을 설정

  • (8) :build task 실행 전에 실행되는 task. :copyDocument task가 수행되면 index.html 파일이 src/main/resources/static/docs 에 copy 되며, copy 된 index.html 파일은 API 문서를 파일 형태로 외부에 제공하기 위한 용도로 사용 가능

  • (8-1) :asciidoctor task가 실행된 후에 task가 실행되도록 의존성을 설정

  • (8-2) "build/docs/asciidoc/" 경로에 생성되는 index.html을 copy한 후,

  • (8-3)의 "src/main/resources/static/docs" 경로로 index.html을 추가

  • (9) :build task가 실행되기 전에 :copyDocument task가 먼저 수행되도록 함

  • (10) 애플리케이션 실행 파일이 생성하는 :bootJar task 설정

  • (10-1)에서는 :bootJar task 실행 전에 :copyDocument task가 실행되도록 의존성을 설정

  • (10-2)와 (10-3)에서는 Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가.
    jar 파일에 index.html을 추가해 줌으로써 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API 문서를 확인 가능

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

3. API 문서 스니펫을 사용하기 위한 템플릿(또는 source 파일) 생성

  • Gradle 기반 프로젝트일 시 src/docs/asciidoc/ 디렉토리 내에 비어있는 템플릿 문서(index.adoc)를 생성해줘야 함

4. 기본 구조

  • API 문서 생성을 위한 테스트 케이스 기본 구조
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;

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

    @MockBean
	  // (5) 테스트 대상 Controller 클래스가 의존하는 객체를 Mock Bean 객체로 주입받기

    @Test
    public void postMemberTest() throws Exception {
        // given
        // (6) 테스트 데이터 

        // (7) Mock 객체를 이용한 Stubbing

        // when
        ResultActions actions =
                mockMvc.perform(
                     // (8) request 전송
                );

        // then
        actions
                .andExpect(// (9) response에 대한 기대 값 검증)
                .andDo(document(
                            // (10) API 문서 스펙 정보 추가
                 ));
    }
}
  • (1) @WebMvcTest(MemberController.class)

    • @SpringBootTest 애너테이션을 사용하지 않고, @WebMvcTest 애너테이션을 사용
    • Controller를 테스트하기 위한 전용 애너테이션
    • 괄호 안에는 테스트 대상 Controller 클래스를 지정
  • (2) @MockBean(JpaMetamodelMappingContext.class)

    • JPA에서 사용하는 Bean 들을 Mock 객체로 주입해 주는 설정
      • Spring Boot 기반의 테스트는 항상 최상위 패키지 경로에 있는 xxxxxxxApplication 클래스를 찾아서 실행
        - @EnableJpaAuditing 사용시 JPA와 관련된 Bean을 필요로 하기 때문에 @WebMvcTest 애너테이션을 사용해서 테스트를 진행할 경우에는 (2)와 같이 JpaMetamodelMappingContext를 Mock 객체로 주입
  • (3) Spring Rest Docs에 대한 자동 구성을 위해 @AutoConfigureRestDocs를 추가

  • (4) MockMvc 객체를 주입

  • (5) Controller 클래스가 의존하는 객체(서비스 클래스, Mapper)의 의존성을 제거하기 위해 @MockBean 애너테이션을 사용해서 Mock 객체를 주입

  • (6) HTTP request에 필요한 request body나 query parmeter, path variable 등의 데이터를 추가

  • (7) (5)에서 주입받은 Mock 객체가 동작하도록 Mockito에서 지원하는 given() 등의 메서드로 Stubbing

  • (8) MockMvc의 perform() 메서드로 request를 전송

  • (9) response를 검증

  • (10) 테스트 수행 이후, API 문서를 자동 생성하기 위한 해당 Controller 핸들러 메서드의 API 스펙 정보를 document(…)에 추가

    • andDo() 메서드 andExpect()처럼 어떤 검증 작업을 하는 것이 아닌 일반적인 동작을 정의
    • document(…) 메서드는 API 문서를 생성하기 위해 Spring Rest Docs에서 지원하는 메서드

@SpringBootTest vs @WebMvcTest

  • @SpringBootTest

    • @AutoConfigureMockMvc과 함께 사용되어 Controller를 테스트
    • 프로젝트에서 사용하는 전체 Bean을 ApplicationContext에 등록하여 사용
    • 테스트 환경을 구성하는 것은 편리하지만 실행 속도가 상대적으로 느림
    • 데이터베이스까지 요청 프로세스가 이어지는 통합 테스트에 주로 사용
  • @WebMvcTest

    • Controller 테스트에 필요한 Bean만 ApplicationContext에 등록
    • 실행 속도가 상대적으로 빠름
    • Controller에서 의존하고 있는 객체가 있다면 해당 객체에 대해서 Mock 객체를 사용하여 의존성을 일일이 제거해줘야 함
    • Controller를 위한 슬라이스 테스트에 주로 사용

Spring Rest Docs 적용

1. 코드 추가

1) postMemberTest()

import static com.codestates.util.ApiDocumentUtils.getRequestPreProcessor;
import static com.codestates.util.ApiDocumentUtils.getResponsePreProcessor;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.mockito.BDDMockito.*;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.request.RequestDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {

    @Autowired
    private MockMvc mockMvc;

//    (1)
    @MockBean
    private MemberService memberService;

//    (2)
    @MockBean
    private MemberMapper mapper;

    @Autowired
    private Gson gson;

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

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

//        (5)
        Member mockResultMember = new Member();
        mockResultMember.setMemberId(1L);
        given(memberService.createMember(Mockito.any(Member.class)))
                .willReturn(mockResultMember);

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

//        then
        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v11/members/"))))
                .andDo(document( // (7)
                        "post-member", // (7-1)
                        getRequestPreProcessor(), // (7-2)
                        getResponsePreProcessor(), // (7-3)
                        requestFields( // (7-4)
                                List.of(
                                        fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), // (7-5)
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호")
                                )
                        ),
                        responseHeaders( // (7-6)
                                headerWithName(HttpHeaders.LOCATION).description("Location header. 등록된 리소스의 URI")
                        )
                ));
    }
  • (1), (2) MemberService와 MemberMapper의 Mock Bean을 주입받아 의존하는 객체와 관계 단절
  • (3) postMember() 핸들러 메서드에 전송할 request body
  • (4), (5) 의존하는 객체의 메서드 호출을 (1)(2)에서 주입받은 Mock 객체를 사용해서 Stubbing
  • (6) POST 요청 전송
  • (7) document() API 스펙 정보를 전달받아서 실질적인 문서화 작업을 수행
  • (7-1) API 문서 스니펫의 식별자 역할, 문서 스니펫은 지정한 "post-member" 디렉토리 하위에 생성
  • (7-2), (7-3) request와 response에 해당하는 문서 영역을 전처리하는 역할
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;

import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;

public interface ApiDocumentUtils {
   static OperationRequestPreprocessor getRequestPreProcessor() {
       return preprocessRequest(prettyPrint());
    }

    static OperationResponsePreprocessor getResponsePreProcessor() {
        return preprocessResponse(prettyPrint());
    }
}
  • (7-4) requestFields()는 문서로 표현될 request body를 의미, 파라미터로 전달되는 List의 원소인
    FieldDescriptor 객체가 request body에 포함된 데이터를 표현
  • (7-5) type(JsonFieldType.STRING)은 JSON 프로퍼티의 값이 문자열임을 의미
  • (7-6) responseHeaders() 문서로 표현될 response header를 의미, 파라미터로 전달되는 HeaderDescriptor 객체가 response header를 표현

2) patchMemberTest()

    @Test
    public void patchMemberTest() throws Exception {
//        given
        long memberId = 1L;
        MemberDto.Patch patch = new MemberDto.Patch(memberId, "홍길동", "010-2222-2222", Member.MemberStatus.MEMBER_ACTIVE);
        String content = gson.toJson(patch);

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

//        willReturn()이 최소한 null은 아니어야 한다
        given(mapper.memberPatchToMember(Mockito.any(MemberDto.Patch.class))).willReturn(new Member());
        given(memberService.updateMember(Mockito.any(Member.class))).willReturn(new Member());
        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(responseDto);

//        when
        ResultActions actions =
                mockMvc.perform(
                        patch("/v11/members/{member-id}", memberId)
                                .accept(MediaType.APPLICATION_JSON)
                                .contentType(MediaType.APPLICATION_JSON)
                                .content(content)
                );

//        then
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.memberId").value(patch.getMemberId()))
                .andExpect(jsonPath("$.data.name").value(patch.getName()))
                .andExpect(jsonPath("$.data.phone").value(patch.getPhone()))
                .andExpect(jsonPath("$.data.memberStatus").value(patch.getMemberStatus().getStatus()))
                .andDo(document("patch-member",
                        getRequestPreProcessor(),
                        getResponsePreProcessor(),
                        pathParameters( // (1)
                                parameterWithName("member-id").description("회원 식별자")
                        ),
                        requestFields(
                                List.of(
                                        fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(), // (2)
                                        fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional(), // (3)
                                        fieldWithPath("phone").type(JsonFieldType.STRING).description("휴대폰 번호").optional(),
                                        fieldWithPath("memberStatus").type(JsonFieldType.STRING).description("회원 상태: MEMBER_ACTIVE / MEMBER_SLEEP / MEMBER_QUIT").optional()
                                )
                        ),
                        responseFields( // (4)
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                        fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"), // (5)
                                        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("스탬프 개수")
                                )
                        )
                ));
    }
  • (1) URL의 path variable의 정보를 추가
    • /v11/members/{member-id}”와 같이 요청 URL에 path variable이 포함되어 있음
  • (2) API 스펙 정보에서 제외
    • memberId의 경우, path variable 정보로 memberId를 전달받기 때문에 MemberDto.Patch DTO 클래스에서 request body에 매핑되지 않는 정보
  • (3) optional()을 추가해서 API 스펙 정보에서 필수가 아닌 선택 정보로 설정
  • (4) responseFields(…)는 문서로 표현될 response body를 의미, 파라미터로 전달되는 List의 원소인 FieldDescriptor 객체가 response body에 포함된 데이터를 표현
  • (5) fieldWithPath("data.memberId")의 data.memberId 는 data 프로퍼티의 하위 프로퍼티를 의미

3) getMemberTest()

    @Test
    public void getMemberTest() throws Exception {
//        given
        Member member = new Member();
        member.setMemberId(1L);

        MemberDto.Response response = new MemberDto.Response(
                1L,
                "hgd@gmail.com",
                "홍길동",
                "010-1111-1111",
                Member.MemberStatus.MEMBER_ACTIVE,
                new Stamp()
        );
        given(memberService.findMember(Mockito.anyLong())).willReturn(member);
        given(mapper.memberToMemberResponse(Mockito.any(Member.class))).willReturn(response);

//        when
        mockMvc.perform(
                        get("/v11/members/{member-id}", member.getMemberId())
                                .accept(MediaType.APPLICATION_JSON)
                )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.email").value(response.getEmail()))
                .andExpect(jsonPath("$.data.name").value(response.getName()))
                .andExpect(jsonPath("$.data.phone").value(response.getPhone()))
                .andExpect(jsonPath("$.data.memberStatus").value(response.getMemberStatus()))
                .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("스탬프 개수"))
                        )
                ));
    }

4) getMembersTest()

    @Test
    public void getMembersTest() throws Exception {
        Member member1 = new Member("hgd1@gmail.com", "길동1", "010-1111-1111");
        member1.setMemberStatus(Member.MemberStatus.MEMBER_ACTIVE);
        member1.setStamp(new Stamp());

        Member member2 = new Member("hgd2@gmail.com", "길동2", "010-2222-2222");
        member2.setMemberStatus(Member.MemberStatus.MEMBER_ACTIVE);
        member2.setStamp(new Stamp());

        Page<Member> pageMembers =
                new PageImpl<>(List.of(member1, member2),
                        PageRequest.of(0, 10,
                                Sort.by("memberId").descending()), 2);

        List<MemberDto.Response> responses = List.of(
                new MemberDto.Response(1L,
                        "hgd1@gmail.com",
                        "길동1",
                        "010-1111-1111",
                        Member.MemberStatus.MEMBER_ACTIVE,
                        new Stamp()),

                new MemberDto.Response(2L,
                        "hgd2@gmail.com",
                        "길동2",
                        "010-2222-2222",
                        Member.MemberStatus.MEMBER_ACTIVE,
                        new Stamp())
        );
        given(memberService.findMembers(Mockito.anyInt(), Mockito.anyInt())).willReturn(pageMembers);
        given(mapper.membersToMemberResponses(Mockito.anyList())).willReturn(responses);

        String page = "1";
        String size = "10";
//        (1)
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("page", page);
        queryParams.add("size", size);

        ResultActions actions =
                mockMvc.perform(
                        get("/v11/members")
                                .params(queryParams) // (2)
                                .accept(MediaType.APPLICATION_JSON)
                );
        actions
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").isArray())
                .andDo(document(
                        "get-members",
                        getRequestPreProcessor(),
                        getResponsePreProcessor(),
                        requestParameters(
                                parameterWithName("page").description("Page 번호"),
                                parameterWithName("size").description("Page 사이즈")
                        ),
                        responseFields(
                                List.of(
                                        fieldWithPath("data").type(JsonFieldType.ARRAY).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("스탬프 개수"),
                                        fieldWithPath("pageInfo").type(JsonFieldType.OBJECT).description("Page 정보"),
                                        fieldWithPath("pageInfo.page").type(JsonFieldType.NUMBER).description("Page 번호"),
                                        fieldWithPath("pageInfo.size").type(JsonFieldType.NUMBER).description("Page 사이즈"),
                                        fieldWithPath("pageInfo.totalElements").type(JsonFieldType.NUMBER).description("전체 데이터 수"),
                                        fieldWithPath("pageInfo.totalPages").type(JsonFieldType.NUMBER).description("전체 페이지 수")

                                )
                        )
                ));
    }
  • (1) MultiValueMap
    • page와 size를 담기 위해 중복 value를 허용하는 MultiValueMap 사용
  • (2) params() 쿼리 파라미터 정보 전송

5) deleteMemberTest()

    @Test
    public void deleteMemberTest() throws Exception {
        Long memberId = 1L;
        doNothing().when(memberService).deleteMember(Mockito.anyLong());

        ResultActions actions = mockMvc.perform(
                delete("/v11/members/{member-id}", memberId)
        );

        actions.andExpect(status().isNoContent())
                .andDo(document(
                        "delete-member",
                        getRequestPreProcessor(),
                        getResponsePreProcessor(),
                        pathParameters(
                                parameterWithName("member-id").description("회원 식별자")
                        )
                ));
    }

2. 문서 스니펫 확인

  • 테스트 케이스 실행 후 문서 스니펫 자동 생성

3. 스니펫을 이용해 API 문서 만들기

  • src/docs/asciidoc” 디렉토리를 생성하고 비어 있는 “index.adoc” 파일을 생성

  • 작성 예시

= 커피 주문 애플리케이션 // (1) API 문서의 제목

// (2) API 문서의 목차와 관련된 내용
:sectnums:
:toc: left
:toclevels: 4
:toc-title: Table of Contents
:source-highlighter: prettify

Jung-seo   // (3) API 문서를 생성한 이의 정보

v1.0.0, 2022.07.03    // (4) API 문서의 생성 날짜

// (5) 생성한 API 문서 스니펫을 사용하는 부분
***
== MemberController
=== 회원 등록
.curl-request
include::{snippets}/post-member/curl-request.adoc[]

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

.request-fields
include::{snippets}/post-member/request-fields.adoc[]

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

.response-headers
include::{snippets}/post-member/response-headers.adoc[]

=== 회원 정보 수정
.curl-request
include::{snippets}/patch-member/curl-request.adoc[]

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

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

.request-fields
include::{snippets}/patch-member/request-fields.adoc[]

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

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

=== 회원 정보 조회
.curl-request
include::{snippets}/get-member/curl-request.adoc[]

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

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

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

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

.response-body
include::{snippets}/get-member/response-body.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[]

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

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

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

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

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

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

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

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

4. 템플릿 문서를 HTML 파일로 변환

  • :bootJar 또는 :build task 실행

Asciidoc

1. 기본 문법

  • Spring Rest Docs를 통해 생성되는 텍스트 기반 문서 포맷
  • 기술 문서 작성을 위해 설계된 가벼운 마크업 언어
= 커피 주문 애플리케이션     // (1)
:sectnums:                  // (2)
:toc: left                  // (3)
:toclevels: 4               // (4)
:toc-title: Table of Contents   // (5)
:source-highlighter: prettify   // (6)
  • (1) 문서의 제목을 작성하기 위해서는 =를 추가. ====와 같이 =의 개수가 늘어날수록 작아짐

  • (2) 목차에서 각 섹션에 넘버링을 해주기 위해 :sectnums: 를 추가

  • (3) :toc: 는 목차를 문서의 어느 위치에 구성할 것인지를 설정. 문서의 왼쪽에 목차가 표시되도록 left를 지정

  • (4) :toclevels: 은 목차에 표시할 제목의 level을 지정. 4로 지정했기 때문에 ==== 까지의 제목만 목차에 표시

  • (5) :toc-title: 은 목차의 제목을 지정 가능

  • (6) :source-highlighter: 문서에 표시되는 소스 코드 하이라이터를 지정

  • 추가 사항

    • "***" 단락을 구분 지을 수 있는 수평선을 추가
      • 문단의 제목 다음에 한 라인을 띄우고 한 칸 들여 쓰기 후 문단을 작성하면 박스 문단을 사용
    • CAUTION: 을 사용해서 경고 문구를 추가
    • NOTE: , TIP: , IMPORTANT: , WARNING: 등을 사용 가능
    • http/https/ftp/irc/mailto/hgd@gmail.com 등 URL Scheme은 자동 인식하여 링크 설정
    • image::URL로 이미지 추가 가능

2. Asciidoctor

  • AsciiDoc 포맷의 문서를 파싱 해서 HTML 5, 매뉴얼 페이지, PDF 및 EPUB 3 등의 문서를 생성하는 툴
  • Spring Rest Docs에서는 Asciidoc 포맷의 문서를 HTML 파일로 변환하기 위해 내부적으로 Asciidoctor를 사용
  • 템플릿 문서에 include된 스니펫은 애플리케이션 빌드 시 내부적으로 Asciidoctor가 index.adoc을 index.html로 변환 후, 특정 디렉토리(src/main/resources/static/docs)에 생성
***
== MemberController
=== 회원 등록
.curl-request       // (1)
include::{snippets}/post-member/http-request.adoc[]    // (2)

.request-fields
include::{snippets}/post-member/request-fields.adoc[]

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

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

...
  • (1) .curl-request 에서 .은 하나의 스니펫 섹션 제목을 표현
  • (2) include는 스니펫을 템플릿 문서에 포함할 때 사용
    • {snippets}는 해당 스니펫이 생성되는 디폴트 경로를 의미

0개의 댓글