Springboot restdocs 적용기[2] 실전 사용
이전엔 기본적인 사용법에 대해서 알아봤습니다. 이번엔 custom 하고, 리펙토링을 진행해보겠습니다.
1. build.gradle 수정
이전과 달라진 부분만 주석으로 설명을 적겠습니다.
plugins {
id 'org.springframework.boot' version '2.6.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
// config 추가
asciidoctorExtensions
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 추가 adoc 파일들을 연산으로 사용할 수 있게 해주며, html로 export할 수 있게 해줌
asciidoctorExtensions "org.springframework.restdocs:spring-restdocs-asciidoctor"
}
tasks.named('test') {
useJUnitPlatform()
}
ext {
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
useJUnitPlatform()
}
asciidoctor {
dependsOn test
// config 추가
configurations 'asciidoctorExtensions'
inputs.dir snippetsDir
// 변환할 adoc들을 명시한다.
sources{
include("**/*.adoc")
}
// include 연산을 사용할 때 base 디렉터리를 지정해줍니다.
baseDirFollowsSourceFile()
}
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument
}
이전과 달라진 부분이라면 asciidoctorExtensions를 사용합니다. 이 역할은 adoc을 대상으로 연산을 진행할 수 있도록 해주며, html로 export 할 수 있는 역할을 합니다.
2. RestDocsConfiguration 변경
이전에는 그저 prettyPrint()만 사용했지만, 우리는 앞에서 사용할 때 document의 이름을 계속 명시해줬어야 했습니다.
ex) document("members", ~~~) 이러한 문제를 해결하기 위해 아래와 같이 변경합니다.
@TestConfiguration
public class RestDocsConfiguration {
@Bean
public RestDocumentationResultHandler restDocsMockMvcConfigurationCustomizer() {
return MockMvcRestDocumentation.document("{class-name}/{method-name}",
Preprocessors.preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()));
}
}
이제 document는 테스트 class 이름+테스트 메서드 이름으로 snippets이 생성되므로 항상 이름을 명시하지 않아도 됩니다.
3. RestDocsBasic
이전에는 Test 클래스마다 @AutoConfigureRestDocs를 적고 MockMvc를 사용했지만, 이러한 공통 부분을 묶고 Mockmvc 커스텀을 진행해줍니다. 이제 모든 문서화를 진행할 Test 코드들은 이 클래스를 상속받아 사용합니다.
@Import(RestDocsConfiguration.class)
@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(MemberController.class)
public class RestDocsBasic {
@Autowired
MockMvc mvc;
@Autowired
RestDocumentationResultHandler restDocs;
@Autowired
ObjectMapper mapper;
@BeforeEach
void setUp(WebApplicationContext context, RestDocumentationContextProvider provider){
this.mvc= MockMvcBuilders.webAppContextSetup(context)
.apply(MockMvcRestDocumentation.documentationConfiguration(provider))
.alwaysDo(MockMvcResultHandlers.print())
.alwaysDo(restDocs)
.addFilters(new CharacterEncodingFilter("UTF-8",true))
.build();
}
protected String createStringJson(Object dto) throws JsonProcessingException {
return mapper.writeValueAsString(dto);
}
}
4. MemberControllerTest 예시
하나의 예시만 살펴보겠습니다. 이전과 달라진 부분은 andDo()부분에서 위에서 만든 RestDocumentationResultHandler의 docuemnt를 사용해서 class name + class method로 이름을 자동으로 생성해주는 역할을 합니다. 나머지는 동일합니다.
@Test
void test() throws Exception {
FieldDescriptor[] reviews = getReviewFieldDescriptors();
List<MemberResponseDto.ListDto> list = new ArrayList<>();
list.add(new MemberResponseDto.ListDto("fsd",10));
list.add(new MemberResponseDto.ListDto("fsddf",10));
Mockito.when(memberService.findAll()).thenReturn(list);
ResultActions resultActions1 = mvc.perform(MockMvcRequestBuilders.get("/members")
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("fsd"))
.andDo(restDocs.document(PayloadDocumentation.responseFields(reviews)));
}
5. index.adoc 수정
이전에는 include 연산을 이용해서 하나의 adoc을 명시해서 사용했지만 operation을 이용해서 원하는 조각들을 한 번에 명시할 수 있습니다. 여기서는 http-request, http-response, response-fields를 조각으로 사용합니다.
= API 문서 (글의 제목)
Test용 (부제)
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left // 문서의 목차를 왼쪽에 부여한다.
:toclevels: 2
:sectlinks:
[[API-LIST]] // 해당 텍스트에 링크를 건다.
== APIs // 제목으로 링크가 걸립니다.
[[Member Find List API]]
=== Member Find List API
operation::member-controller-test/test[snippets="http-request,http-response,response-fields"]
이때 Member 관련 API가 길어진다면 adoc을 분리해서 사용할 수 있습니다. 아래와 같이 index.adoc에서 member관련 부분을 변경해줍니다.
include::Member-API.adoc[]
그리고 Member-API.adoc[]을 제작해줍니다. 이렇게 하면 index.adoc에 모든 설정을 넣는 것이 API마다 조각을 분리해서 사용할 수 있습니다.
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[Member-API]]
== Member API
[[Member-리스트-조화]]
=== Member 리스트 조회
operation::member-controller-test/test[snippets='http-request,http-response,response-fields']
6. html 방식으로 수정
위의 방식으로 진행한다면, index.adoc이 결국은 한 페이지로 엄청나게 길어집니다. 이러한 현상을 해결하기 위해 위에서 html로 변경할 수 있도록 라이브러리를 추가했습니다. 따라서 html로 변환되는 것들을 볼 수 있습니다. 이제 한 페이지가 아닌 html로 새창을 띄울 수 있도록 index.adoc을 변경합니다.
* link: html명[보이고 싶은 html 이름] 이렇게하면 html로 이동하게 됩니다. 옆에 적은 window=blank는 새로운 창으로 띄우도록 하는 설정입니다.
= API 문서 (글의 제목)
Test용 (부제)
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
include::overview.adoc[]
[[API-LIST]]
== APIs
[[Member-Find-List-API]]
=== Member Find List API
* link:Member-API.html[member-Api, window=blank]
7. Test
설정은 모두 끝났으니 build하고, 확인해줍니다. 아래와 같이 목차로 이동할 수 있는 텝이 생기고, 한 페이지에서 모든 것을 보는 게 아닌 창으로 이동해서 볼 수 있어 가독성이 올라간 것을 볼 수 있습니다.
8. 공통 설정
이제 예외나 오류 메시지는 공통된 포멧으로 동작하게 합니다. 따라서 Test에 Restdocs를 사용할 수 있도록 Controller를 만들어서 Test를 진행합니다. 해당 Controller는 운영환경에서는 동작하지 않습니다.
예외를 발생시킬 Controller
@RestController
public class ErrorDocController {
@PostMapping("/error")
public void errorSample(@Validated @RequestBody ExRequest exRequest){
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class ExRequest{
@NotEmpty
private String name;
@Email
private String email;
}
}
Restdocs를 만들 테스트
여기서 오류때 반환하는 객체를 커스텀해서 사용하시면 됩니다. 현재는 Bean Validation에서 걸렸을 때를 가정한 것입니다.
@Test
void error() throws Exception {
ErrorDocController.ExRequest exRequest = new ErrorDocController.ExRequest("hi", "h123");
mvc.perform(
post("/error")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(exRequest))
)
.andExpect(status().isBadRequest())
.andDo(
restDocs.document(
responseFields(
fieldWithPath("timestamp").description("시각"),
fieldWithPath("status").description("Error Code"),
fieldWithPath("error").description("Error 값 배열 값")
)
)
);
}
9. overview.adoc
오류 공통으로 쓰일 API를 명시해줍니다.
:doctype: book
:icons: font
:source-highlighter:
:toc: left
:toclevels: 2
:sectlinks:
[[overview]]
== Overview
[[overview-host]]
=== Host
|===
| 환경 | Host
| Beta
| `localhost`
| Production
| '191.232.141.222 ex임'
|===
[[overview-http-status-codes]]
=== HTTP status codes
|===
| 상태 코드 | 설명
| `200 OK`
| 성공
| `400 Bad Request`
| 잘못된 요청
| `401 Unauthorized`
| 비인증 상태
| `403 Forbidden`
| 권한 거부
| `404 Not Found`
| 존재하지 않는 요청 리소스
| `500 Internal Server Error`
| 서버 에러
|===
[[overview-error-response]]
=== HTTP Error Response
operation::[snippets='http-response,response-fields']
10. Index.adoc 수정
API들 위에 오류의 공통으로 쓰일 조각을 include 해줍니다.
include::overview.adoc[]
11. 최종 Restdocs
지금까지 Restdocs를 만들고, Custom 하는 방법까지 알아봤습니다 감사합니다.
참고
https://docs.spring.io/spring-restdocs/docs/current/reference/html5/
https://techblog.woowahan.com/2597/
'SpringBoot' 카테고리의 다른 글
Springboot 모니터링 사용기[2] Prometheus, Grafana (0) | 2022.06.14 |
---|---|
Springboot 모니터링 사용기[1] actuator (0) | 2022.06.12 |
Springboot restdocs 적용기[1] gradle 7.x (0) | 2022.06.04 |
Springboot Log 남기기 (0) | 2022.06.01 |
SpringBoot [스프링부트] 시작하기(11) League 내부 로직 만들기 (0) | 2022.01.18 |
댓글
이 글 공유하기
다른 글
-
Springboot 모니터링 사용기[2] Prometheus, Grafana
Springboot 모니터링 사용기[2] Prometheus, Grafana
2022.06.14 -
Springboot 모니터링 사용기[1] actuator
Springboot 모니터링 사용기[1] actuator
2022.06.12 -
Springboot restdocs 적용기[1] gradle 7.x
Springboot restdocs 적용기[1] gradle 7.x
2022.06.04 -
Springboot Log 남기기
Springboot Log 남기기
2022.06.01