[TIL] API Documentation 1
코드스테이츠 백엔드 부트캠프 D+84
요번주도 한주가 지나간다.
얼마 공부한 것 같은느낌이 아닌데 벌써
월요일이지나 금요일이 되었다.
저번주에 본 리그오브레전드 월즈 결승전을 감명깊게 보아서 그런지
내 마음 한켠에 불안하고 안정적이지 못한 지금의 삶에
많이 지쳐하고 있었는데, 많은 위로가 되었다.
‘중요한건 꺽이지 않는 마음’ 참 잘 만든 문구인 것 같다.
나도 꺽이지 않고 나아가는 모습을 발견할 수있도록
더욱더 노력해보아야겠다.
3일간 JUnit에 대한 공부를 했다.
여러 API를 사용해보면서 계층별로 단독으로
테스트할 수 있는 Mockito라는 좋은 API를 배웠다.
오늘은 이것에 연장선상인 API 문서화에 대해 공부해보는 날이다.
Spring Rest Docs
API 문서화라고하면
클라이언트에서 REST API 백엔드 애플리케이에
요청을 전송하기 위해서 알아 되는 요청 정를 문서로 정리한 것이다.
(URI, request body, query parameter 등..)
Spring Rest Docs는 이 문서를 자동으로 만들어주는 API이다.
기존에는 Swagger 오픈 API를 많이 이용하였다고 한다.
Spring Rest Docs 문서 생성 흐름
테스트 코드 작성
- test 테스크 실행
- 테스트 결과 Passed / Failed 1번으로 복귀
- API 문서 스니펫 생성(.adoc)
- API 문서 생성(.adoc)
- API 문서 -> HTML로 변환
위와 같은 흐름으로 API 문서가 생성되며
테스트 케이스에 통과한 테스트의 API 문서 스니핏이 생성된다.
*스니핏: 문서의 일부 조각이라 생각하면 된다.
Spring Rest Docs를 사용하기 위해 사전 작업을 하여야한다.
build.gradle의 설정해주자
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
...
}
...
ext {
set('snippetsDir', file("build/generated-snippets")) // 스니펫 생성 경로 설정
}
configurations {
asciidoctorExtensions // AsciiDoctor 의존그룹 지정
}
dependencies {
// 의존라이브러리 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
...
}
// test task실행 시, API 문서 생성 스니핏 경로를 설정
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
// Asciidoctor 기능을 사용하기 위한 설정
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
// index.html copy
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("${asciidoctor.outputDir}")
into file("src/main/resources/static/docs")
}
// copyDocument가 먼저 실행되도록 설정
build {
dependsOn copyDocument
}
// bootJar task 설정
bootJar {
dependsOn copyDocument
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
build.gradle 설정이 끝났다면 이제 코드를 작성해보자
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
//@Transactional
//@SpringBootTest
//@AutoConfigureMockMvc
@AutoConfigureRestDocs
public class MemberControllerRestDocsTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private MemberService memberService;
@MockBean
private MemberMapper mapper;
@Autowired
private Gson gson;
@Test
public void postMemberTest() throws Exception {
MemberDto.Post post = new MemberDto.Post("dhfif718@naver.com", "이재혁", "010-1234-5678");
String content = gson.toJson(post);
MemberDto.response responseDto = new MemberDto.response(
1L,
"dhfif718@naver.com",
"이재혁",
"010-1234-5678",
Member.MemberStatus.MEMBER_ACTIVE,
new Stamp()
);
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);
ResultActions actions = mockMvc.perform(
post("/api/members")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(content)
);
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(
"post-member",
getRequestPreProcessor(),
getResponsePreProcessor(),
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("스탬프 갯수")
)
)
));
}
}
기존 코드와 달라진점은
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
어노테이션으로 변경하였다는 점이다.
이전에 SpringBootTest와 AutoConfigureMockMvc를 사용했던 부분을
변경하였다. WebMveTest로 변경 함으로써 Controller 계층에 사용하는
Bean만 등록하기때문에 상대적으로 속도가 빠르다고 한다.
또한, @AutoConfigureRestDocs 어노테이션을 붙여주었고
.andDo(docment())를 이용해 Rest Docs에 필요한 정보를 보내주었다.
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
document(), fieldWithPath(), requestFields()
필요한 정보를 만드는 과정에서 위 api를 사용하기위해선
PayloadDocumentation, MockMvcRestDocumentation을 import 해주어야한다.
그리고 patch 핸들러 메서드에 대해 path parameter를 이용할 경우
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
를 스태틱으로 import 해주면 사용이 가능하다.
Document()의 매개변수로 전달하기위해
OperationRequestPreprocessor
OperationResponsePreprocessor
클래스를 이용해 인터페이스안에 정적 메서드를 만들어 구현하였다.
public interface ApiDocumentUtils {
static OperationRequestPreprocessor getRequestPreProcessor() {
return preprocessRequest(prettyPrint());
}
static OperationResponsePreprocessor getResponsePreProcessor() {
return preprocessResponse(prettyPrint());
}
}
getRequestPreProcessor()
getResponsePreProcessor()
아리는 메서드를 만들었고 해당 내용엔 OperationPreprocessor 클래스의
메서드인 prettyPrint()로 객체를 주입해주었다.
API 문서를 생성 전 전처리를 수행하는 기능이라는데
해당 내용에 대해서는 조금더 깊은 학습이 필요해 보인다.
또한 Document() 매개변수인
requestFields와 responseFields를 List.of로
문서화 시킬 내용을 설명과 함께 담아 전달하도록
내용을 작성하였다.
여기까지 작성후 테스트 케이스를 실행하면
우리가 build.gralde에서 ext로 설정해놓은 경로
build/generated-snippets 안에 스니핏 식별자로
post-member로 지정해여 해당 폴더안에 .adoc 파일이 생긴 모습을 볼 수 있다.
실제 내용을 확인해보면 우리가 원하는
API 내용들이 담겨져있다.
그리고 Gradle 프로젝트의 경우
src/docs/asciidoc 폴더 경로에서 index.adoc 파일을 생성한다.
index.adoc파일에 Asciidoc 문법으로
템플릿 코드를 넣어 Gradle의 :build, :bootJar을
실행시켜주면 index.html
gradle의 설정했던 task copyDocument의 경로인
src/main/resources/static/docs 안에 index.html 파일로
우리의 API문서가 변환된 모습을 볼 수 있다.
그리고 최종적으로 Spring Boot를 실행시켜
index.html에 접근해보면
우리가 원하는 모습대로 API 문서가 작성된 모습을 확인해 볼 수 있다.
(해당 템플릿 코드는 코드스테이츠 교육자료로 제공된 템플릿이므로
템플릿 코드는 따로 올리지 않겠습니다!)
이렇게 오늘은 간단한 것 같으면서도 어려운…
SpringRestDocs를 사용하는 법을 알아보았고
초기 셋팅만 gradle에 잘 해놓고 템플릿 코드만 준비되어있다면
크게 어려운점은 없었던 것 같다.
매우 유익했던 Test케이스를 이용한 API 자동화 작업.
엄청 신기하고 재밌었다.
오늘 공부는 여기서 끝 !!
오늘의 커피량: ☕️ ☕️
오늘의 점심: 핫도그, 빵