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
댓글을 사용할 수 없습니다.