이전엔 기본적인 사용법에 대해서 알아봤습니다. 이번엔 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/