프런트와 같이 프로젝트를 효율적으로 진행하려면 API 문서를 공유해야 합니다. restdocs를 통해서 손쉽게 만들 수 있습니다. 

 

1. Springboot restdocs란?

 

Spring Rest Docs는 테스트 코드를 기반으로 자동으로 API 문서를 작성할 수 있게 도와주는 프레임워크입니다.

물론 작성하는 방법이 API 문서를 만들어주는 Swagger보다 쉽지 않습니다. 하지만 반드시 테스트 코드 작성을 요하고, Test를 통과해야 된다는 장점이 있습니다. Test 코드가 필수로 되는 시기에 좋은 기술인 거 같습니다. 한 번 적용해보겠습니다. 

 

 

2. build.gradle 수정

 

restdocs를 사용하기 위해서는 디펜던시를 추가하며, 몇 가지 작업을 해줘야 합니다. 추가해보겠습니다. 

 

2-1. plugins 추가

 

여기서 약간의 삽질을 했습니다. gradle 7 이전에는 org.asciidoctor.convert을 플러그인으로 추가했지만 gradle 7 이후에는 asciidoctor.jvm.convert를 추가해줘야 합니다. 

 

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'
}

 

2-2. 디펜더시 추가

 

restdocs를 추가해줍니다. 이때 위에서 설명했듯이 Test 코드를 토대로 만들기 때문에 Implementation이 아니라 testImplementation입니다. 

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    // restdocs 부분 추가
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

 

2-3. 설정 사항

 

설정 사항에 대한 것은 설명은 주석으로 남기겠습니다. 

// 기존에 만들어진 adoc 삭제 
asciidoctor.doFirst {
    delete file('src/main/resources/static/docs')
}

// 생성물이 snippetsDir에 생성됩니다. 
test {
    useJUnitPlatform()
    outputs.dir snippetsDir
}

// asciidoctor 설정입니다. dependsOn은 Task가 의존하는 것을 명시합니다. 
// 따라서 위에만든 test Task를 의존합니다. 
asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

// 변수 선언
ext {
    snippetsDir = file('build/generated-snippets')
}

// bootjar과 관련된 설정이며, 스니펫을 이용해 문서 작성 후,
// build - docs - asciidoc 하위에 생기는 html 파일을 BOOT-INF/classes/static/docs로 복사해줍니다.
bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'BOOT-INF/classes/static/docs'
    }
}


// asciidoctor를 의존하고, from file 경로에 있는 파일을 into file로 복사를 진행합니다. 
task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}


// build시 copyDocument를 의존합니다. 
build {
    dependsOn copyDocument
}

 

2-4. 최종 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 {
    compileOnly {
        extendsFrom annotationProcessor
    }
}


repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}


tasks.named('test') {
    useJUnitPlatform()
}

ext {
    snippetsDir = file('build/generated-snippets')
}

bootJar {
    dependsOn asciidoctor
    copy {
        from "${asciidoctor.outputDir}"
        into 'BOOT-INF/classes/static/docs'
    }
}

test {
    useJUnitPlatform()
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

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
}

 

 

3. index.adoc

 

API 문서를 만들 때 틀이 되는 adoc파일을 만들어줘야 합니다. 위치는 src/docs/asciidoc/에 만들어주고, 파일명은 상관없습니다. 

 

 

 

 

밑의 파일로 적어주시면 됩니다. 커스터마이징이 가능합니다. 따라서 이 링크로 가서 문법에 대해서 살펴보며 자신만의 API문서를 만들 수 있습니다. https://narusas.github.io/2018/03/21/Asciidoc-basic.html

ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

== REQUEST

include::{snippets}/members/http-request.adoc[]

== RESPONSE

include::{snippets}/members/http-response.adoc[]

== RESPONSE HEADER

include::{snippets}/members/response-headers.adoc[]

== RESPONSE FIEDL

include::{snippets}/members/response-fields.adoc[]

 

4. RestDocsConfiguration

 

Restdocs를 사용하면서 각각의 표시하려는 컨텐트마다 가로선을 부여할 수 있도록 suffix를 지정하는 Configuration입니다. 모든 Controller에 대해서 사용할 예정입니다. 

 

@TestConfiguration
public class RestDocsConfiguration {
    @Bean
    public RestDocsMockMvcConfigurationCustomizer restDocsMockMvcConfigurationCustomizer() {
        return configurer -> configurer.operationPreprocessors()
                .withRequestDefaults(prettyPrint())
                .withResponseDefaults(prettyPrint());
    }
}

 

 

5. RestDocsController

 

각각의 Controller에 대해서 작성하시는 게 좋습니다. 저는 예시로 MemberController에 대해서 작성해보겠습니다. 

설명은 주석으로 남기겠습니다. 

@AutoConfigureRestDocs // restdocs 활성화 해주는 어노테이션
@WebMvcTest(MemberController.class) // MemberController에 대해서만 진행
@Import(RestDocsConfiguration.class) // 이전에 작성한 suffix관련 bean 사용
public class RestDocs {

    @Autowired
    MockMvc mvc;

    @MockBean
    MemberServiceImpl memberService;

    @Test
    void test() throws Exception {
		
        // response field 설명 명세  
        FieldDescriptor[] reviews = getReviewFieldDescriptors();
		
        // 협력자 응답
        List<MemberResponseDto.ListDto> list = new ArrayList<>();
        list.add(new MemberResponseDto.ListDto("fsd",10));
        Mockito.when(memberService.findAll()).thenReturn(list);
        
        // GET /members로 요청을 보낸다. 
        ResultActions resultActions1 = mvc.perform(MockMvcRequestBuilders.get("/members").header(HttpHeaders.AUTHORIZATION,"hihi")
                .accept(MediaType.APPLICATION_JSON))
                // 응답에 대한 검증을 진행하고, andDo 부분에서 문서화를 진행한다.
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("fsd"))
                // 가장 먼저 adoc와 매칭될 document를 명시한다. 위에서 members로 정했습니다. 
                .andDo(MockMvcRestDocumentation.document("/members",
                        // 응답에 field값에 대해 명세한다. 
                        // 현재는 따로 만들어서 했지만, PayloadDocumentation.fieldWithPath().description(),~~
                        // 로 진행해도 된다.
                        PayloadDocumentation.responseFields(reviews),
                        // 요청 헤더의 명세를 한다.
                        HeaderDocumentation.requestHeaders(HeaderDocumentation.headerWithName(HttpHeaders.AUTHORIZATION).description("hasdf")),
                        // 응답 헤더의 명세를 한다.
                        HeaderDocumentation.responseHeaders(HeaderDocumentation.headerWithName("hihi").description("헤더"))));

    }

    private FieldDescriptor[] getReviewFieldDescriptors() {
        return new FieldDescriptor[]{
                fieldWithPath("[]name").description("이름"),
                fieldWithPath("[]age").description("나이")
        };
    }
}

 

6. 생성 확인

 

전부 끝마쳤다면 프로젝트의 gradlew.bat이 있는 곳에서 terminal을 켜줍니다. 저는 git bash로 진행했습니다.

 

테스트가 전부 성공적으로 끝났다면, 아래와 build가 완료되었다고 표시됩니다. 

'

 

아래와 같이 모두 index.html과 build/generated-snippets/members에 adoc이 생성됐는지 확인하고, index.html을 열어 확인합니다. 

 

정상적으로 문서가 완성됐습니다. index.adoc을 수정하여 필요한 API에 대해서 추가하거나, Custom해서 가독성을 높게 만들 수도 있습니다. 

 

지금까지 restdoc으로 API 문서를 제작해봤습니다. 감사합니다. 

 

Test코드에서 쉽게 따라하실 수 있도록 static을 import 하지 않았습니다. 실제로 작성하실 때는 static을 import 해주세요:)

 

모든 코드는 아래의 링크에서 확인하실 수 있습니다.

https://github.com/rlaehdals/TestCode