원활한 업무 진행을 위해 정리해두려는 글입니다.
참고
Spring 공식 문서 Creating API Documentation with Restdocs
restdocs의 개념은 대충 알고 있으니 바로 시작하자.
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'io.spring.dependency-management' version '1.0.12.RELEASE'
id 'java'
//restdocs 용 asciidoctor
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
...
// restdocs 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
ext {
snippetsDir = file('build/generated-snippets') //(3) 빌드시 snippets 파일들이 저장될 저장소
}
tasks.named('test') {
useJUnitPlatform()
outputs.dir snippetsDir
}
asciidoctor { //(5) asccidoctor 설정
dependsOn test
inputs.dir snippetsDir
}
asciidoctor.doFirst { //(6) asciidoctor가 실행될 때 docs 하위 파일 삭제
delete file('src/main/resources/static/docs')
}
bootJar { //(7) bootJar 시 asciidoctor 종속되고 build하위 스니펫츠 파일을 classes 하위로 복사
dependsOn asciidoctor
copy {
from "${asciidoctor.outputDir}"
into 'BOOT-INF/classes/static/docs'
}
}
task copyDocument(type: Copy) { //(8) from의 파일을 into로 복사
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
build { //(9) build 시 copyDocument 실행
dependsOn copyDocument
}
gradle 설정을 다음과 같이 적용했다.
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.operation.preprocess.Preprocessors;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kakaovx.ballmateapitest.dto.product.ProductInsertDto;
@AutoConfigureRestDocs
@AutoConfigureMockMvc
@SpringBootTest
@Transactional(readOnly = true)
class ProductInte {
@Autowired
private MockMvc mock;
@Test
void product_list_성공() throws Exception {
ResultActions perform = mock.perform(MockMvcRequestBuilders.get("/product/all"));
perform.andExpect(MockMvcResultMatchers.status().isOk());
//perform.andDo(MockMvcResultHandlers.print());
perform.andDo(MockMvcRestDocumentation.document("product/all",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint()),
PayloadDocumentation.responseFields(
PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과 코드"),
PayloadDocumentation.fieldWithPath("data.[].id").type(JsonFieldType.NUMBER).description("아이디 값"),
PayloadDocumentation.fieldWithPath("data.[].create_date").type(JsonFieldType.STRING).description("생성일"),
PayloadDocumentation.fieldWithPath("data.[].modified_date").type(JsonFieldType.STRING).description("수정일"),
PayloadDocumentation.fieldWithPath("data.[].email").type(JsonFieldType.STRING).description("이메일"),
PayloadDocumentation.fieldWithPath("data.[].name").type(JsonFieldType.STRING).description("이름"),
PayloadDocumentation.fieldWithPath("data.[].picture").type(JsonFieldType.STRING).description("데이터")
)
));
}
}
{
"code": 0,
"data": [
{
"id": 1,
"create_date": "2022-08-04T22:10:44.712903",
"modified_date": "2022-08-04T22:10:44.712903",
"email": "juno@mail.com",
"name": "한글",
"picture": "picture"
},
{
"id": 2,
"create_date": "2022-08-04T22:10:44.712903",
"modified_date": "2022-08-04T22:10:44.712903",
"email": "juno@mail.com",
"name": "깨짐",
"picture": "phone"
},
{
"id": 3,
"create_date": "2022-08-04T22:10:44.712903",
"modified_date": "2022-08-04T22:10:44.712903",
"email": "juno@mail.com",
"name": "확인",
"picture": "cup"
},
{
"id": 4,
"create_date": "2022-08-04T22:10:44.712903",
"modified_date": "2022-08-04T22:10:44.712903",
"email": "juno@mail.com",
"name": "용도",
"picture": "watch"
}
]
}
반환할 형식의 데이터에 맞게 다음과 같이 작성해준다.
ifndef::snippets[]
:snippets: ./build/snippets
endif::[]
= TEST API
:toc: left
:toclevels: 4
:toc-title: test api
== API TEST
=== REQUEST
include::{snippets}/product/all/http-request.adoc[]
=== REQEUST FIELD
include::{snippets}/product/all/request-fields.adoc[]
=== RESPONSE
include::{snippets}/product/all/http-response.adoc[]
=== RESPONSE FIELD
include::{snippets}/product/all/response-fields.adoc[]
adoc 파일은 위 gradle에서 설정한 /src/docs/asciidoc/index.adoc
파일로 작성했다.
./gradlew clean build
를 통해 테스트 코드가 모두 실행되면
그냥 테스트 코드만 실행해도 되지만 build 시에 파일이 나온다는 것을 보여주기 위해!
다음과 같이 resources/static/docs/index.html
파일이 생성된다. 해당 파일을 접근하기 위해
http://localhost:8080/docs/index.html
url로 호출을 하면
화면이 나오는데 다음과 같이 한글이 깨진 것을 확인할 수 있었다.
한글 깨짐 및 최적화 문제를 해결해보자!
참고
Spring REST Docs 적용 및 최적화 하기
위 내용으로 이제 우리는 docs 파일을 자동으로 생성할 수 있다. 하지만 코드가 너무 길어지고 한글이 깨지는 듯 여러 문제들이 발생하고 있다. 해당 문제를 해결해보자.
@TestConfiguration
public class RestDocsConfig {
@Bean
public RestDocumentationResultHandler handler(){
return MockMvcRestDocumentation.document(
"{class-name}/{method-name}",
Preprocessors.preprocessRequest(Preprocessors.prettyPrint()),
Preprocessors.preprocessResponse(Preprocessors.prettyPrint())
);
}
public static final Attribute field(String key, String value){
return new Attribute(key,value);
}
}
다음 파일을 test/{project pacakge}/config/
아래에 생성해주었다.
여기서 field는 추후에 문서를 커스텀할 때 filed값이 추가된다면 사용할 옵션이기 때문에 사용할 때 설명하겠다. 지금 궁금한 분들은 위 참고 링크를 타고 들어가서 확인해보면 된다.
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kakaovx.ballmateapitest.config.RestDocsConfig;
import com.kakaovx.ballmateapitest.dto.product.ProductInsertDto;
@AutoConfigureRestDocs
@AutoConfigureMockMvc
@SpringBootTest
@Import(RestDocsConfig.class)
@ExtendWith(RestDocumentationExtension.class)
@Transactional(readOnly = true)
class ProductInte {
@Autowired
private MockMvc mock;
@Autowired
private ObjectMapper objectMapper;
@Autowired
protected RestDocumentationResultHandler docs;
@BeforeEach
void setUp(final WebApplicationContext context,
final RestDocumentationContextProvider provider) {
this.mock = MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider)) // rest docs 설정 주입
.alwaysDo(MockMvcResultHandlers.print()) // andDo(print()) 코드 포함 -> 3번 문제 해결
.alwaysDo(docs) // pretty 패턴과 문서 디렉토리 명 정해준것 적용
.addFilters(new CharacterEncodingFilter("UTF-8", true)) // 한글 깨짐 방지
.build();
}
@Test
@DisplayName("product 리스트를 불러오는데 성공한다.")
void productAll() throws Exception {
ResultActions perform = mock.perform(MockMvcRequestBuilders.get("/product/all").contentType(MediaType.APPLICATION_JSON));
perform.andExpect(MockMvcResultMatchers.status().isOk());
perform.andDo(docs.document(
PayloadDocumentation.responseFields(
PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("결과 코드"),
PayloadDocumentation.fieldWithPath("data.[].id").type(JsonFieldType.NUMBER).description("아이디 값"),
PayloadDocumentation.fieldWithPath("data.[].create_date").type(JsonFieldType.STRING).description("생성일"),
PayloadDocumentation.fieldWithPath("data.[].modified_date").type(JsonFieldType.STRING).description("수정일"),
PayloadDocumentation.fieldWithPath("data.[].email").type(JsonFieldType.STRING).description("이메일"),
PayloadDocumentation.fieldWithPath("data.[].name").type(JsonFieldType.STRING).description("이름"),
PayloadDocumentation.fieldWithPath("data.[].picture").type(JsonFieldType.STRING).description("데이터")
)
));
}
}
그리고 기존 코드를 다음과 같이 변경하여 작성하였다.
여기서 주의해야할 코드는
@Import(RestDocsConfig.class)
우리가 작성한 RestDocsConfig 파일을 읽어올 수 있게 해준다.@ExtendWith(RestDocumentationExtension.class)
setUp에서 주입받을 RestDocumentationContextProvider를 읽어올 수 있게 해준다.andDo(docs.document())
해당 부분이 기존에 Restdosc 자체 메서드를 사용하는 것이 아닌 우리가 설정한 docs를 불러와서 출력해야한다.
이제 한글이 깨지지 않고 출력값도 잘 가져올 수 있었다.