API 문서화
API 문서화 목적
API 문서 생성 자동화 필요성
Spring Rest Docs vs Swagger
Spring Rest Docs
Spring Rest Docs 설정
plugins {
...
// .adoc 파일 확장자를 가지는 AsciiDoc 문서를 생성해주는 Asciidoctor를 사용하기 위한 플러그인을 추가
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 {
// spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리가 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// spring-restdocs-asciidoctor 의존 라이브러리를 추가
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
...
}
// 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
// index.html 파일이 copy됨
task copyDocument(type: Copy) {
// asciidoctor task가 실행된 후에 task가 실행 되도록 의존성을 설정
dependsOn asciidoctor
// build/docs/asciidoc/" 경로에 생성되는 index.html을 copy
from file("${asciidoctor.outputDir}")
// src/main/resources/static/docs 경로로 index.html을 추가
into file("src/main/resources/static/docs")
}
// build 실행 전, copyDocument task가 먼저 수행 되도록 한다.
build {
dependsOn copyDocument
}
bootJar {
// bootJar task 실행 전에 copyDocument task가 실행 되도록 의존성을 설정
dependsOn copyDocument
// Asciidoctor 실행으로 생성되는 index.html 파일을 jar 파일 안에 추가
// 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API 문서를 확인 가능
from ("${asciidoctor.outputDir}") { // (10-2)
into 'static/docs' // (10-3)
}
}
Spring Rest Docs 적용
...
// 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 부분이 Spring Rest Docs
.andDo(document("patch-member", // 문서 식별자
preprocessRequest(prettyPrint());, // 요청 데이터 이쁘게 보여줌
preprocessResponse(prettyPrint());, // 응답 데이터 이쁘게 보여줌
pathParameters(
parameterWithName("member-id").description("회원 식별자")
),
requestFields(
List.of(
fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
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("스탬프 갯수")
)
)
));
Snippets 종류
// query Parameter 표기
requestParameters(
parameterWithName("page").description("page 번호")
)
// path Parameter 표기
// 이름이 ResultAction에 사용한 주소와 일치
// ex) delete("/v11/members/{member-id}", memberId)
pathParameters(
parameterWithName("member-id").description("회원 식별자")
)
// 요청 데이터 표기
// ignored() 를 통해 Dto 일부 제거, optional() 을 통해 필수 여부 표기 가능
requestFields(
fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
fieldWithPath("name").type(JsonFieldType.STRING).description("이름").optional()
)
// 응답 데이터 표기
responseFields(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
// JSON 배열 표기 [] 만 사용
fieldWithPath("data[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자")
)
주의사항