API 문서화란?
클라이언트가 REST API 백엔드 애플리케이션에 요청을 전송하기 위해서 알아야 하는 요청 정보(URI, request body, query parameter 등)를 문서로 정리하는 것을 의미한다.
API 문서는 개발자가 요청 URL(또는 URI) 등의 API 정보를 직접 수기로 작성할 수도 있고, 애플리케이션 빌드를 통해 API 문서를 자동 생성할 수도 있다.
Spring Rest Docs
Spring Rest Docs는 REST API 문서를 자동으로 생성해 주는 Spring 하위 프로젝트이다.
Spring Rest Docs를 사용한 API 문서화의 대표적인 장점은 테스트 케이스에서 전송하는 API 문서 정보와 Controller에서 구현한 Request Body, Response Body, Query Parmeter 등의 정보가 하나라도 일치하지 않으면 테스트 케이스의 실행 결과가 “failed” 되면서 API 문서가 정상적으로 생성이 되지 않는다는 것이다.
따라서 API 스펙 정보와 API 문서 정보의 불일치로 인해 발생하는 문제를 방지할 수 있고, 결국엔 "passed"이면 Controller에 정의되어 있는 Request Body나 Response Body 등의 API 스펙 정보와 일치하는 API 문서가 만들어진다는 것이다.
Spring Rest Docs의 대표적인 단점이라면 테스트 케이스를 일일이 작성해야 되고, Controller에 대한 모든 테스트 케이스를 “passed”로 만들어야 한다는 점이다.
Spring Rest Docs의 API 문서 생성 흐름
1. 테스트 코드 작성
- Controller 슬라이스 테스트 코드를 먼저 작성한다.
- Controller에 정의 되어 있는 API 스펙 정보(Request Body, Response Body, Query Parameter 등)를 코드로 작성한다.
2. test 태스크 실행
- Gradle의 빌드 태스크 중 하나인 test task를 실행 시켜 API 문서 스니핏을 일괄 생성한다.
- 테스트 실행 결과가 "passed"이면 다음 작업을 진행하고, "failed"이면 문제를 해결하기 위한 테스트 케이스를 수정하고 다시 진행한다.
3. API 문서 snippet(.adoc 파일) 생성
- 테스트 케이스 실행 결과가 "passed"이면 테스트 코드에 포함된 API 스펙 정보 코드를 기반으로 API 문서 스니핏이 .adoc 파일로 생성된다.
스니핏(snippet)이란?
코드의 일부 조각 혹은 문서의 일부 조각을 의미한다. 스니핏은 테스트 케이스 하나 당 하나의 스니핏이 생성되며, 여러개의 스니핏을 모아서 하나의 API 문서를 생성할 수 있다.
4. API 문서 생성
- 생성된 API 문서 스니핏을 모아서 하나의 API 문서로 생성한다.
5. API 문서를 HTML로 변환
- 생성된 API 문서를 HTML 파일로 변환한다.
- 변환된 API 문서는 HTML 파일 자체로 공유할 수도 있고, URL을 통해 해당 HTML에 접속하여 확인할 수 있다.
Spring Rest Docs 적용하기
1. build.gradle 설정
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
// .adoc 확장자를 가지는 AsciiDoc 문서 생성 플러그인
id "org.asciidoctor.jvm.convert" version "3.3.2"
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 {
// restdocs-core, mockmvc 의존 라이브러리 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// 위에서 지정한 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 실행 시, 기능을 사용하기 위해 asciidoctorExtentions을 설정한다.
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
// build task 실행 전에 실행되는 task이다.
// 아래 경로에 index.html 파일이 copy 되고, 해당 파일은 API 문서를 파일 형태로 외부에 제공하기 위한 용도로 사용한다.
task copyDocument(type: Copy) {
dependsOn asciidoctor // asciidoctor task가 실행된 후에 task가 실행되도록 의존성을 설정한다.
from file("${asciidoctor.outputDir}") // build/docs/asciidoc/ 경로에 생성되는 index.html을 copy 한다.
into file("src/main/resources/static/docs") // 해당 경로로 index.html을 추가해 준다.
}
build {
dependsOn copyDocument // build task가 실행되기 전에 copyDocument task가 먼저 수행되도록 한다.
}
// 애플리케이션 실행 파일이 생성하는 bootJar task 설정이다.
bootJar {
dependsOn copyDocument // bootJar task 실행 전에 :copyDocument task가 실행 되도록 의존성을 설정한다.
from ("${asciidoctor.outputDir}") { // Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가해 준다.
into 'static/docs'
}
}
2. Controller 테스트 케이스에 Spring Rest Docs 적용하기
2-1 Root Application 클래스에 @EnableJpaAuditing 애노테이션 추가
@EnableJpaAuditing
@SpringBootApplication
public class Section3Week3RestDocsApplication {
public static void main(String[] args) {
SpringApplication.run(Section3Week3RestDocsApplication.class, args);
}
}
@WebMvcTest 애너테이션을 사용해서 테스트를 진행 할 경우에는 @EnableJpaAuditing 을 Root Application 클래스에 추가하여 Controller 테스트 케이스에 JPA에서 사용하는 Bean들을 Mock 객체로 주입해주어야 한다. Controller 테스트 케이스에 @MockBean(JpaMetamodelMappingContext.class) 애너테이션을 추가하면 된다.
2-2 Controller 테스트 케이스에 Spring Rest Docs 적용
@WebMvcTest(MemberController.class) // Controller 테스트 전용 애너테이션. 괄호안에 테스트 대상 클래스 지정
@MockBean(JpaMetamodelMappingContext.class) // JPA에서 사용하는 Bean들을 Mock 객체로 주입해준다. Root Application 클래스를 찾아서 실행한다.
@AutoConfigureRestDocs // Spring Rest Docs 자동 구성
public class MemberControllerRestDocsTest {
@Autowired
private MockMvc mockMvc; // MockMvc 객체 주입
@MockBean // 테스트 대상 Controller 클래스가 의존하는 객체를 Mock Bean으로 주입 받는다.
private MemberService memberService;
@MockBean
private MemberMapper mapper;
@Autowired
private Gson gson;
@Test
public void postMemberTest() throws Exception {
// given
// HTTP request에 필요한 request body나 query parmeter, path variable 등의 데이터를 추가한다.
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1111-2222");
String content = gson.toJson(post);
MemberDto.response responseDto = new MemberDto.response(1L, "hgd@gmail.com",
"홍길동", "010-1111-2222", Member.MemberStatus.MEMBER_ACTIVE, new Stamp());
// Mock 객체가 동작하도록 Mockito에서 지원하는 given()등의 메서드로 Stubbing 한다.
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( // request 전송
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() 메서드는 API 문서를 생성하기 위한 Spring Rest Docs에서 지원하는 메서드이다.
.andDo(document( // API 문서 스펙 정보 추가
"post-member", // API 문서 스니핏의 식별자 역할을 한다. 지정한 디렉토리 하위에 문서 스니핏이 생성된다.
getRequestPreProcessor(), // request 문서 영역 전처리
getResponsePreProcessor(), // response 문서 영역 전처리
requestFields( // 문서로 표현될 request body의 path, type, description(설명)을 포함한다.
List.of(
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"), // JSON 포맷의 프로퍼티를 의미한다.
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("phone").type(JsonFieldType.STRING).description("번호")
)
),
responseFields( // 문서로 표현될 response body의 path, type, description(설명)을 포함한다.
List.of(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"), // 프로퍼티 값이 객체임을 의미한다.
fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"), // data 프로퍼티의 하위 프로퍼티를 의미한다.
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("스탬프")
)
)
));
}
@Test
public void patchMemberTest() throws Exception {
// given
long memberId = 1L;
MemberDto.Patch patch = new MemberDto.Patch(memberId, "홍길동", "010-2222-3333", Member.MemberStatus.MEMBER_ACTIVE);
String content = gson.toJson(patch);
MemberDto.response responseDto =
new MemberDto.response(1l, "hgd@gmail.com", "홍길동", "010-2222-3333", Member.MemberStatus.MEMBER_ACTIVE, new Stamp());
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(),
// API 스펙에 path variable 정보 추가.
// memberId는 path variable 정보로 전달 받기 때문에 MemberDto.Patch 클래스에서 request body에 매핑되지 않는 정보이다.
pathParameters(
parameterWithName("member-id").description("회원 식별자")
),
requestFields(
List.of(
fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(), // API 스펙 정보에서 제외
fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional(), // 선택적으로 수정 가능
fieldWithPath("phone").type(JsonFieldType.STRING).description("번호").optional(),
fieldWithPath("memberStatus").type(JsonFieldType.STRING).description("회원 상태: MEMBER_ACTIVE / MEMBER_SLEEP / MEMBER_QUIT").optional()
)
),
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("스탬프")
)
)
));
내용 정리
- @SpringBootTest 는 데이터베이스까지 요청 프로세스가 이어지는 통합 테스트에 주로 사용되고, @WebMvcTest 는 Controller를 위한 슬라이스 테스트에 주로 사용한다.
- document(…) 메서드는 API 스펙 정보를 전달 받아서 실질적인 문서화 작업을 수행하는 RestDocumentationResultHandler 클래스에서 가장 핵심 기능을 하는 메서드이다.
- OperationRequestPreprocessor와 OperationResponsePreprocessor를 이용해 API 문서를 생성 전에 전처리를 수행할 수 있다.
- requestFields(…)는 문서로 표현될 request body를 의미하며, 파라미터로 전달되는 List<FieldDescriptor> 의 원소인 FieldDescriptor 객체가 request body에 포함되는 데이터를 표현한다.
- responseFields(…)는 문서로 표현될 response body를 의미하며, 파라미터로 전달되는 List<FieldDescriptor> 의 원소인 FieldDescriptor 객체가 response body에 포함된 데이터를 표현한다.
3. API 문서 스니핏 템플릿(또는 source 파일) 생성
3-1 index.adoc 탬플릿 생성
Gradle 프로젝트의 경우, 템플릿 문서가 위치하는 디폴트 경로가 “src/docs/asciidoc” 이다.
“src/docs/asciidoc” 디렉토리를 생성하고 비어 있는 “index.adoc” 파일을 생성한다.
3-2 index.adoc 탬플릿 내용 작성
= 커피 주문 애플리케이션 // API 문서의 제목
:sectnums: // 목차
:toc: left
:toclevels: 4
:toc-title: Table of Contents
:source-highlighter: prettify
Gil Dong Hong <hgd@gmail.com> // API 문서를 생성한 이의 정보
v1.0.0, 2022.09.14 // API 문서 생성 날짜
// 테스트 케이스 실행을 통해 생성한 API 문서 스니핏을 사용하는 부분이다.
// include::{snippets}/스니핏 문서가 위치한 디렉토리/스니핏 문서파일명.adoc[] 형식으로 스니핏을 사용한다.
***
== 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-fields
include::{snippets}/post-member/response-fields.adoc[]
=== 회원 정보 수정
.curl-request
include::{snippets}/patch-member/curl-request.adoc[]
.http-request
include::{snippets}/patch-member/http-request.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[]
3-3 템플릿 문서를 HTML 파일로 변환
Gradle 탭에서 bootjar 또는 bulid 명령 더블 클릭하여 변환
src/main/resources/static/docs 디렉토리에서 index.html 파일 생성 확인
이후 애플리케이션을 실행하고 'http://localhost:8080/docs/index.html' URL을 웹 브라우저 입력하여 Spring Rest Docs 기반 API 문서를 확인할 수 있다.
Asciidoc란?
Asciidoc는 Spring Rest Docs를 통해 생성되는 텍스트 기반 문서 포맷이다.
주로 기술 문서 작성을 위해 설계된 가벼운 마크업 언어이다.
Spring Rest Docs를 통해 만들어지는 문서 스니핏과 이 문서 스니핏을 사용하는 탬플릿 문서를 제공하기 위해 API 문서를 이해할 수 있는 수준의 Asciidoc 기본 문법은 알고 있어야 한다.
1. 목차 구성
= 커피 주문 애플리케이션 // (1)
:sectnums: // (2)
:toc: left // (3)
:toclevels: 4 // (4)
:toc-title: Table of Contents // (5)
:source-highlighter: prettify // (6)
Hwang Jung Sik <jungsik.hwang@codestates.com>
v1.0.0, 2022.07.10
(1) 문서의 제목을 작성하기 위해서는 =를 추가하면 된다. ====와 같이 =의 개수가 늘어날 수록 글자는 작아진다.
(2) 목차에서 각 섹션에 넘버링을 해주기 위해서는 :sectnums: 를 추가하면 된다.
(3) :toc: 는 목차를 문서의 어느 위치에 구성할 것인지를 설정한다.
(4) :toclevels: 은 목차에 표시할 제목의 level을 지정한다. 여기서는 4로 지정했기 때문에 ==== 까지의 제목만 목차에 표시된다.
(5) :toc-title: 은 목차의 제목을 지정할 수 있다.
(6) :source-highlighter: 문서에 표시되는 소스 코드 하일라이터를 지정한다.
목차 구성 확인
2. 박스 문단 사용
***
API 문서 개요
이 문서는 Spring MVC 기반의 REST API 기반 애플리케이션에 대한 샘플 API 문서입니다.
***
*** 로 단락을 구분 지을 수 있다.
문단의 제목 다음에 한 칸을 띄우고 한 칸 들여쓰기로 문단을 작성할 수 있다.
박스 문단 확인
3. 경고 문구 추가
***
API 문서 개요
이 문서는 Spring MVC 기반의 REST API 기반 애플리케이션에 대한 샘플 API 문서입니다.
CAUTION: 이 문서는 학습용으로 일부 기능에 제한이 있습니다.
***
CAUTION:, NOTE: , TIP: , IMPORTANT: , WARNING: 등을 사용할 수 있다.
4. URL Scheme 자동 인식
다음과 같은 URL Scheme는 Asciidoc에서 자동으로 인식하여 링크가 설정된다.
- http
- https
- ftp
- irc
- mailto
- hgd@gmail.com
이미지 추가
image::https://spring.io/images/spring-logo-9146a4d3298760c2e7e49595184e1975.svg[spring]
5. 문서 스니핏을 템플릿 문서에 포함 시키기
테스트 케이스 실행을 통해 생성된 문서 스니핏을 템플릿 문서에 포함(include)시키면 애플리케이션 빌드 타임에 Asciidoctor가 index.adoc를 index.html로 변환 후, 특정 디렉토리에 생성해 준다.
이 전 단계에 템플릿 문서에 스니핏을 포함하는 방법을 알아야 한다.
***
== MemberController
=== 회원 등록
.curl-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-fields
include::{snippets}/post-member/response-fields.adoc[]
...
...
.curl-request 에서 .은 하나의 스니핏 섹션 제목을 표현하기 위해 사용되며, curl-request는 섹션 제목이므로 수정이 가능하다.
include:: 는 Asciidoctor에서 사용하는 매크로 중 하나이며, 스니핏을 템플릿 문서에 포함할 때 사용한다.
{snippets} 는 해당 스니핏이 생성되는 디폴트 경로를 의미하며, build.gradle 에서 설정한 snippetDir 변수를 참조 한다.
더 많은 내용 참고
https://docs.asciidoctor.org/asciidoc/latest/
AsciiDoc - AsciiDoc Language Documentation
A documentation page for AsciiDoc.
docs.asciidoctor.org
'Spring Boot > MVC' 카테고리의 다른 글
[Spring MVC] application.yml 프로파일 기능 사용법 (0) | 2022.09.16 |
---|---|
[Spring MVC] 테스팅(Testing) - Mockito (0) | 2022.09.13 |
[Spring MVC] 테스팅(Testing) (0) | 2022.09.08 |
[Spring MVC] 트랜잭션(Transaction) (0) | 2022.09.06 |
[Spring MVC] Spring Data JPA 데이터 액세스 계층 구현 (0) | 2022.09.03 |