Spring

Spring Rest Docs Tutorial

Tommy__Kim 2023. 4. 6. 17:15
backend 개발자들이 API 문서화를 하는 방법에는 여러 가지 방법이 있다.
1. 수기를 통한 문서화
2. postman publish 기능을 통한 문서화
3. swagger를 활용한 문서화
4. Spring REST Docs를 활용한 문서화 
...

각각의 API 문서화마다 장단점이 있을 것이다. 

 

특히 1번, 2번 방식은 별로 추천하지 않는 방식이다. 

[추천하지 않는 이유]

  1. api 명세가 바뀌었을 때 까먹고 바꿔주지 않는다면 api 명세 기능을 잃어버린다. 
  2. 개발자가 실수로 오타를 내었을 때 누군가 알려주지 않는 이상 이를 발견하기 힘들다. 

Swagger와 Rest Docs 방식을 통한 API 문서화 방법 중 이번 장에서는 Spring Rest Docs를 활용한 API 문서화 방법을 알아보려고 한다.

 

Tutorial 환경
spring boot 3.0.5
java : 17
gradle : 7.5

스프링 공식 문서 바로가기

 

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.

docs.spring.io

spring boot 3.x.x 버전대의 경우 위의 링크를 통해 참고하면 됩니다.

 

[build.gralde 설정] 

 

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.5'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'org.asciidoctor.jvm.convert' version '3.3.2'	{1}
}

group = 'practice'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    asciidoctorExtensions
}

repositories {
    mavenCentral()
}

ext {
    set('snippetsDir', file("build/generated-snippets"))
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // Spring Rest Docs 관련 추가
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
	
    // ascii 관련 추가
    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

test {
    useJUnitPlatform()
    outputs.dir snippetsDir
}

asciidoctor {
    inputs.dir snippetsDir
    configurations 'asciidoctorExtensions'
    dependsOn test
}

bootJar {
    dependsOn asciidoctor
    
    // build 경로 안에 있는 index.html을 밖으로 꺼내준다.
    copy {
        from "${asciidoctor.outputDir}"      
        into "src/main/resources/static/docs"   // src/main/resources/static/docs로 복사
    }

}

{1} : Java 17의 경우 org.asciidoctor.convert 가 아닌 org.asciidoctor.jvm.convert를 사용해 주어야 합니다.

나머지 부분들은 코드 안의 주석을 참고하시면 될 것 같습니다. 

 

[간단한 Post Controller code]

 

@RestController
public class PostController {

    @GetMapping("/post")
    public ResponseEntity getPost() {
        Post post = buildPost("RestDocs", "Rest Document에 관한 게시글입니다.");

        PostResponseDto postResponseDto = buildResponseDto(post);
        return ResponseEntity.ok(postResponseDto);
    }

    @PostMapping("/post")
    public ResponseEntity createPost(@RequestBody PostRequestDto postRequestDto) {
        Post post = buildPost(postRequestDto.getName(), postRequestDto.getContent());
        PostResponseDto postResponseDto = buildResponseDto(post);
        return ResponseEntity.ok(postResponseDto);
    }

    private static Post buildPost(String name, String content) {
        return  Post.builder()
                .name(name)
                .content(content)
                .build();
    }

    private static PostResponseDto buildResponseDto(Post post) {
        PostResponseDto postResponseDto = PostResponseDto.builder()
                .name(post.getName())
                .content(post.getContent())
                .build();
        return postResponseDto;
    }
}​

본 글의 경우 Rest Docs에 주 목표가 있으므로 Controller는 최소한의 예제로만 진행했습니다. 

 

하나의 경우는 @GetMapping("/post"), 다른 하나의 경우는 @PostMapping("/post)에 대해서 진행하였습니다.

 

[Post Controller Test Code]

 

@ExtendWith({RestDocumentationExtension.class})
@AutoConfigureMockMvc
@SpringBootTest
class PostControllerTest {

    @Autowired
    MockMvc mockMvc;
    @Autowired
    ObjectMapper objectMapper;

    @BeforeEach
    void setup(WebApplicationContext webApplicationContext,
               RestDocumentationContextProvider restDocumentationContextProvider) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .addFilter(new CharacterEncodingFilter("UTF-8", true))
                .apply(documentationConfiguration(restDocumentationContextProvider)
                        .operationPreprocessors()
                        .withRequestDefaults(modifyUris().host("localhost").removePort(), prettyPrint())
                        .withResponseDefaults(modifyUris().host("localhost").removePort(), prettyPrint()))
                .build();
    }

    @Test
    void getPost() throws Exception {
        this.mockMvc.perform(MockMvcRequestBuilders.get("/post").accept(MediaType.APPLICATION_JSON))	// get("post")검사
                .andExpect(MockMvcResultMatchers.status().isOk())	// request 요청시의 Http Status 검증
                .andDo(MockMvcResultHandlers.print())	// 로그로 request, response 확인 가능 
                .andDo(document("getPost",	// snippet 생성, 이름 = "getPost"
                        HeaderDocumentation.requestHeaders(	// request Header 설명 추가
                                HeaderDocumentation.headerWithName(HttpHeaders.ACCEPT).description("accept header")
                        ),
                        HeaderDocumentation.responseHeaders( // response Header 설명 추가
                                HeaderDocumentation.headerWithName(HttpHeaders.CONTENT_TYPE).description("content type")
                        ),
                        PayloadDocumentation.responseFields( // response Fields 설명 추가
                                PayloadDocumentation.fieldWithPath("name").description("name of post"),
                                PayloadDocumentation.fieldWithPath("content").description("content of post")
                        )
                        ));
    }

    @Test
    void createPost() throws Exception{
        PostRequestDto requestDto = new PostRequestDto("rest docs", "rest docs 실행 예제 입니다.");	//post("post")의 경우 RequestDto 필요

        this.mockMvc.perform(MockMvcRequestBuilders.post("/post")	// post("post") 테스트 
                        .content(objectMapper.writeValueAsString(requestDto))	// requestDto 변환
                        .contentType(MediaType.APPLICATION_JSON)	// MediaType 지정
                        .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andDo(document("create Post",
                        HeaderDocumentation.requestHeaders(
                                HeaderDocumentation.headerWithName(HttpHeaders.ACCEPT).description("accept header")
                        ),
                        PayloadDocumentation.requestFields(
                                PayloadDocumentation.fieldWithPath("name").description("name of post"),
                                PayloadDocumentation.fieldWithPath("content").description("content of post")

                        ),
                        HeaderDocumentation.responseHeaders(
                                HeaderDocumentation.headerWithName(HttpHeaders.CONTENT_TYPE).description("content type")
                        ),
                        PayloadDocumentation.responseFields(
                                PayloadDocumentation.fieldWithPath("name").description("name of post"),
                                PayloadDocumentation.fieldWithPath("content").description("content of post")
                        )
                ));


    }

RestDocs를 진행하기위해선 @ExtendWith(RestDocumentationExtension.class), @AutoConfigureMockMvc, @SpringBootTest가 필요하다.

 

@BeforeEach의 경우 mockMvc의 다양한 기능들을 추가할 수 있는데 이 부분은 앞서 소개한 공식문서에서 찾아보면 알 수 있다. 

  • addFilter 
    • Filter 추가
  • withRequestDefaults(modifyUris().host("localhost").removePort(), prettyPrint())
    • reuqest Header 에서 
      • host : "localhost" 설정
      • port : 제거 
      • prettyPrint() : pretty print 적용 (보다 좀 더 보기 편하게 바꿔준다.)

테스트를 진행하고 나면 

다음과 같이 generated-snippets에 생성이 되는 것을 확인할 수 있다. 

 

 

ASCII 생성 

위 사진과 같이

[src] -> [docs] -> [asciidoc] -> index.adoc  파일을 새로 생성해 준다. 

 

[index.adoc code]

 

= REST DOC EXAMPLE
Rest Documentation 실습
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
:operation-http-request-title: Example request
:operation-http-response-title: Example response

[[POST-API]]
== POST

[[GET-POST]]
=== GET POST
operation::getPost[snippets='http-request,http-response']

[[POST-POST]]
=== CREATE POST
operation::create Post[snippets='http-request,http-response']​

 

operation :: getPost[snippets = 'http-request, http-response']

이 부분은 기존에 테스트코드를 통해 생성했던 generated-snippets를 토대로 생성하는 부분이다. 

 

여러 가지의 adoc파일이 생성되는데 이중 원하는 것들을 나열하면 된다. 

추가적으로 특정 snippet의 이름을 바꾸고 싶다면

:operation-{snippet}-title: 변경 이름으로 바꿔주면 된다. 

 

adoc 파일을 작성을 다했다면 build 를 진행한다. 

그렇게 되면 우리가 명시한 src/main/resources/static/docs/index.html 파일이 생성된다. 

 

[생성된 html] 

 

추가적으로 Multipart / 로그인 관련해서도 문서화가 가능한데 이 부분은 추후에 다시 다뤄보려고 한다.

 

예제 관련 Github Repository : repository 바로 가기 

 

'Spring' 카테고리의 다른 글

Enum Validation  (0) 2023.04.24
Spring Bean의 생명주기  (0) 2023.04.21
싱글톤에 대해서  (2) 2023.04.10
Dependency Injection (의존 관계 주입)  (0) 2023.04.10
DI 와 IOC에 대해서  (0) 2023.04.05