개인 프로젝트는 토이 프로젝트가 많다 보니 아무래도 필요성이 조금 떨어지기는 하지만

현업에서 드는 생각은 API 문서는 필수라는 겁니다.

WEB API를 개발하는 웹 개발자는 본인이 개발하기 때문에 어떤 요청 변수가 필요한지

어떤 응답이 갈지 다 이해하고 있지만 만약 프론트-엔드 파트가 나눠졌다면

프론트-엔드 개발자는 정확히 알기가 어렵습니다.

또한 요새는 B2C 서비스 같은 경우 모바일 서비스는 필수에 가깝기때문에

상황에 따라 AOS/IOS 개발자가 모바일 파트를 담당할 수 있고 개발을 진행하려면 당연히 API 문서가 필수입니다.

API 부재로 구두로 소통하다 보면 당연히 파편화된 기능 개발이 될 수밖에 없습니다.

그리고 시간이 경과함에 따라 인간의 기억은 한계가 있고, 매번 소스를 다시 보면서

API 동작을 파악하는 것도 어렵고 힘든 일입니다.

하지만 매번 API 문서를 작성하는 건 시간적 비용이 상당하고 이미 기 개발된 소스의 경우 더욱 어렵습니다.

문득 자동화할 수 없을까라는 생각이 들어 스쳐 지나가면서 들은 JAVA 생태계에서

대표적인 Spring Rest Docs와 Swagger에 대해 파악해보는 글입니다.

데모를 만들어보고 장단점을 비교해보려고 합니다.

 

1부는 Spring Rest Docs Demo 

2부는 Swagger Demo 

 

모든 소스는 Github에서 확인 가능합니다.

 

※ 개발환경 :  JDK 11, STS 4.8.1,  Spring Boot 2.6.3, Maven 4

 

Spring Rest Docs는 아래 요건을 충족해야합니다. 

해당 환경보다 하위 환경에서는 동작하지 않습니다.

  • 자바 8
  • 스프링 프레임워크 5(5.0.2 이상)

1. Spring Rest Docs Demo 프로젝트 생성

프로젝트, 패키지 명은 적절하게 선택하여 Spring Stater Project 생성합니다.

의존성 설정은 아래의 pom.xml 파일을 참고해주세요.

 

2. Spring Rest Docs Demo 프로젝트 구조

아래 이미지와 같은 구조를 가지게 됩니다.

 

3.  pom.xml 파일 설정

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.3</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>SpringbootRestDocDemo2</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringbootRestDocDemo2</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>11</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.restdocs</groupId>
			<artifactId>spring-restdocs-mockmvc</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.asciidoctor</groupId>
				<artifactId>asciidoctor-maven-plugin</artifactId>
				<version>1.5.8</version>
				<executions>
					<execution>
						<id>generate-docs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>process-asciidoc</goal>
						</goals>
						<configuration>
							<backend>html</backend>
							<doctype>book</doctype>
						</configuration>
					</execution>
				</executions>
				<dependencies>
					<dependency>
						<groupId>org.springframework.restdocs</groupId>
						<artifactId>spring-restdocs-asciidoctor</artifactId>
						<version>2.0.6.RELEASE</version>
					</dependency>
				</dependencies>
			</plugin>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			 <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.outputDirectory}/static/docs
                            </outputDirectory>
                            <resources>
                                <resource>
                                    <directory>
                                        ${project.build.directory}/generated-docs
                                    </directory>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
		</plugins>
	</build>
</project>

 

  •  spring-restdocs-mockmvc 의존성 추가
     MockMvc, WebTestClient, REST Assured 중 MVC 패턴의 API 문서를 위한 것

  • spring-restdocs-asciidoctor 의존성을 추가
    snippet이 사용할 속성으로 .adoc을 가리키도록 자동 구성됩니다.

  • maven-resources-plugin의 prepare-package 추가
    리소스 플러그인을 통해  jar 파일에 /generated-docs 생성된 파일을 포함하게 설정합니다.

4. DemoController 

src/main/java > com.example.demo.controller 패키지를 생성하고 DemoController 클래스를 작성합니다.

package com.example.demo.controller;

import java.util.HashMap;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

	   @GetMapping("/demo/user/{id}")
	   public ResponseEntity<?> getUserInfoById(@PathVariable("id") String id){

		   HashMap<String, String> map = new HashMap<>();
		   map.put("id", id);
		   map.put("name", "jack");
		   map.put("email", "jack@abc.com");

	       return new ResponseEntity<>(map, HttpStatus.OK);
	   }

	   @PostMapping("/demo/user")
	   public ResponseEntity<?> registUserInfo(@RequestParam HashMap<String, String> map) {
		   return new ResponseEntity<>(map, HttpStatus.OK);
	   }
}

 

  • /demo/user/{id}
    user 정보를 식별자(id)에 따라 조회하는 API입니다.
    Demo 테스트 용으로  id, name, email 값을 고정값을 세팅하여 리턴합니다.

  • /demo/user
    Demo 테스트 용으로 user 정보를 등록하는 API로 map 포맷으로 요청 변수를 받아서 리턴합니다.


5. DemoTest

src/test/java > com.example.demo.controller 패키지를 생성하고 DemoTest 클래스를 작성합니다.

Junit 5 기반으로 작성했으며, 해당 링크를 보시면 Junit 4 코드도 확인 가능합니다.

package com.example.demo.controller;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

//import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
//import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
//import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.*;

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@SpringBootTest
public class DemoTest {
		
     private MockMvc mockMvc;

     @BeforeEach
     public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                //.alwaysDo(document("{method-name}/{class-name}", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint())))
                .build();
     }
    
     @Test
	 public void getUserInfoById() throws Exception{
	       this.mockMvc.perform(RestDocumentationRequestBuilders.get("/demo/user/{id}", "user1"))
	       			.andDo(print())
			       .andExpect(status().isOk())
			       .andDo(document("get-user-info-by-Id", 
			    		   pathParameters( 
								parameterWithName("id").description("유저 고유번호") 
			               ),
			    		   responseFields(
			                    fieldWithPath("id").description("유저 고유번호"),
			                    fieldWithPath("name").description("유저 명"),
			                    fieldWithPath("email").description("유저 이메일")
			               )
			       ));
	  }
     
 	@Test
	 public void registUserInfo() throws Exception{
	      this.mockMvc.perform(post("/demo/user")
	    		  .param("id", "user2")
	    		  .param("name","paul")
	    		  .param("email","paul@gmail.com")
	    		  )
	      		  .andDo(print())
	              .andExpect(status().isOk())
	              .andDo(document("regist-user-info",
	                       responseFields(
	                               fieldWithPath("id").description("유저 고유번호"),
	                               fieldWithPath("name").description("유저 명"),
	                               fieldWithPath("email").description("유저 이메일")
	                              )
	              ));
	  }
}

 

  • setup 메서드
    mockMvc 인스턴스를 사용하여 구성. 
    스니펫은 기본적으로 6개 curl, http-request, http-resonse, httpie-request, request-body, resonse-body를 생성.
    /target/generated-snippets/~ 에서 생성된 스니펫 확인 가능합니다.

  • getUserInfoById 메서드
    DemoController의 /demo/user/{id} API의 테스트 코드입니다.
    pathParameter는 @Pathvariable로 받는 id 값의 테스트를 위한 설정인데
    사용할 경우 mockMvcBuilder를 사용하면 컴파일 타임에서 괜찮지만 mvc install 과정에서 Exception이 발생합니다.

    urlTemplate not found.
    If you are using MockMvc did you use RestDocumentationRequestBuilders to build the request?

    메시지 권고대로 mockMvcBuilder 대신 RestDocumentationRequestBuilders를 사용하면 에러가 발생하지 않습니다.

  • registUserInfo 메서드
    DemoController의 /demo/user API의 테스트 코드입니다.

  • 기타
    @ExtendWith는 Junit5 설정이다. spring rest docs 공식 레퍼런스에도 잘 설명되어있습니다.
    Junit4와 설정 차이가 좀 있으니 서두에서 이야기한 Junit4 Demo와 비교해보기를 바랍니다.

6. api-docs.adoc

/src/main/asciidoc 경로에 파일을 생성합니다.

해당 파일을 통해 html 파일을 생성합니다.
여기서는 api-docs.html 파일이 /target/generated-docs 경로에 생성됩니다.

 Demo Project API 명세 (Spring REST Docs)

== getUserInfoById
​
Curl request

include::{snippets}/get-user-info-by-id/curl-request.adoc[]
​
HTTP request
​
include::{snippets}/get-user-info-by-id/http-request.adoc[]

Path Parameter

include::{snippets}/get-user-info-by-id/path-parameters.adoc[]

HTTP Response
​
include::{snippets}/get-user-info-by-id/http-response.adoc[]
​
Response Fields

include::{snippets}/get-user-info-by-id/response-fields.adoc[]
​
Request Body
​
include::{snippets}/get-user-info-by-id/request-body.adoc[]
​
Response Body
​
include::{snippets}/get-user-info-by-id/response-body.adoc[]


== registUserInfo
​
Curl Request
​
include::{snippets}/regist-user-info/curl-request.adoc[]
​
HTTP Request
​
include::{snippets}/regist-user-info/http-request.adoc[]


HTTP Response
​
include::{snippets}/regist-user-info/http-response.adoc[]

Response Fields

include::{snippets}/regist-user-info/response-fields.adoc[]
​
Request Body
​
include::{snippets}/regist-user-info/request-body.adoc[]
​
Response Body
​
include::{snippets}/regist-user-info/response-body.adoc[]

 

7. 테스트

mvn install을 수행하여 테스트를 정상 통과하면 빌드가 완료됩니다.

만약 테스트 실패 시 api-docs.html 파일은 생성되지 않으며,

api-docs.adoc의 작성에 따라 알아보기 힘든 포맷의 파일이 생성되기도 하니 주의해야 합니다.

아래 이미지는 빌드 완료 후 생성된 파일 정보입니다.

 

taget folder

 

이제 생성된 api-docs.html을 브라우저에서 열어보면 아래 이미지와 같이 API 정보가 내용을 확인할 수 있습니다.

 

7. 마치며

Spring Rest Docs Demo를 만들고 결과물까지 확인해봤습니다.

Demo를 작성하면서 느낀 Spring Rest Docs의 장단점을 써보겠습니다.

 

  • 장점
    테스트 코드를 통해 작성된 문서이기 때문에 신뢰도가 높다.
    프로덕션 코드는 건들지 않는다.
  • 단점
    Rest Docs, Junit, Asciidoc의 학습이 필요.
    만약 TDD 환경이 아닌 곳은 테스트 코드 작성 필요.

 

다음 2부에서는 Swagger Demo를 제작하고 장단점을 비교해보겠습니다.

 

※ 참고

https://docs.spring.io/spring-restdocs/docs/2.0.6.RELEASE/reference/html5

https://www.baeldung.com/spring-rest-docs

+ Recent posts