API 문서화
API 문서화 필요이유
API 문서 생성 자동화 필요이유
Spring Rest Docs vs Swagger
Spring Rest Docs
build.gradle 설정
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에서 사용되는 의존 그룹 지정
// :asciidoctor task 실행 시 지정한 'asciidoctorExtensions' 라는 그룹을 지정
configurations {
asciidoctorExtensions
}
dependencies {
// spring-restdocs-core와 spring-restdocs-mockmvc 의존 라이브러리 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// spring-restdocs-asciidoctor 의존 라이브러리 추가
// configurations 에서 지정한 그룹에 의존 라이브러리가 포함됨
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
...
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된 index.html 파일은 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") // src/main/resources/static/docs 경로에 index.html 추가
}
build {
dependsOn copyDocument // :build task 실행 전 :copyDocument task 가 먼저 수행되도록 함
}
// :bootJar task 설정. 웹브라우저에서 API문서 확인 위한 용도
bootJar {
// :bootJar task 실행 전 :copyDocument task 실행되도록 의존성 설정
dependsOn copyDocument
// Asciidoctor 실행으로 생성되는 index.html파일을 jar 파일 안에 추가
// 웹 브라우저에서 접속(http://localhost:8080/docs/index.html) 후, API문서 확인 가능
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
@Autowired
private MockMvc mockMvc;
// MemberService의 Mock Bean 주입
// 테스트 케이스에서의 가짜 메서드 호출 시 사용(Stubbing)
@MockBean
private MemberService memberService;
// MemberMapper의 Mock Bean 주입
// 테스트 케이스에서의 가짜 메서드 호출 시 사용(Stubbing)
@MockBean
private MemberMapper mapper;
@Autowired
private Gson gson;
@Test
public void postMemberTest() throws Exception {
// postMember() 핸들러 메서드에 전송하는 request body
// given
MemberDto.Post post = new MemberDto.Post("hgd@gmail.com", "홍길동", "010-1234-5678");
String content = gson.toJson(post);
// postMember() 핸들러 메서드가 응답으로 전송하는 response body
MemberDto.response responseDto =
new MemberDto.response(1L,
"hgd@gmail.com",
"홍길동",
"010-1234-5678",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp());
// mockito
// 주입받은 mock 객체를 사용하여 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);
// 슬라이스 테스트
// MockMvc의 perform()메서드로 PATCH 요청 전송
ResultActions actions =
mockMvc.perform(
post("/v11/members/{member-id}")
.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.email").value(post.getEmail()))
.andExpect(jsonPath("$.data.name").value(post.getName()))
.andExpect(jsonPath("$.data.phone").value(post.getPhone()))
/// andDo(document... 부터 API 문서 생성 위한 코드
.andDo(document( // API 스펙 정보를 전달받아 실질적 문서화 작업을 수행하는 핵심 메서드
"patch-member", // API 문서 스니핏의 식별자
getRequestPreProcessor(),
getResponsePreProcessor(),
pathParameters(
parameterWithName("member-id").description("회원 식별자")
),
requestFields(
List.of(
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("phone").type(JsonFieldType.STRING).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("스탬프 갯수")
)
)
));
}
}
pathParameters 사용 시 주의