Spring REST docs 작성법
Spring으로 작성된 코드를 아래형식으로 문서화 파일을 만들어보자.
소개
Spring REST docs는 restful 서비스의 문서를 정교하고 읽기 쉽게 하는데 도움을 주는데 목적이 있다.
기본적으로 asciidoctor를 사용하지만 markdown으로 구성을 할 수도 있다.
Spring MVC의 테스트 프레임워크로 작성된 테스트케이스로 스니펫(snippets)을 사용한다.
Spring의 기본적인 설명을 하면 내용이 너무 길어지므로 REST docs 작성을 위한 내용이 아니면 제외하였다.
필요사항
- Java 8
- Spring Framework 5
빌드 환경 구성
빌드환경은 maven으로 구성을 해보겠다.
gradle로 구성할 경우 spring 공식문서를 참조하길 바란다.
[pom.xml]
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<version>2.0.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.7.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>2.2.2</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
<attributes>
<snippets>${project.build.directory}/generated-snippets</snippets>
</attributes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- spring-boot-starter-web는 spring controller를 만들기 위한 의존성으로 사용된다.
- spring-restdocs-mockmvc는 restdocs를 작성하기 위한 것
서비스 개발
Spring REST docs를 사용하기 위해서는 controller서비스가 필요하다. 그래서 테스트 서비스를 만들어 볼 것이다.
사용예제는 사용자(user) 서비스를 만들어서 CRUD에 해당되는 서비스를 restdocs로 만드는 예제로 사용하겠다.
api
- 리스트:
GET
/users - 조회:
GET
/users/{userId} - 등록:
POST
/users - 수정:
PUT
/users/{userId} - 삭제:
DELETE
/users/{userId}
service 작성
@Service
public class UserService {
public List<UserRdo> findList() {
return Arrays.asList(UserRdo.sample());
}
public UserRdo find(String userId) {
return UserRdo.sample();
}
public void register(UserCdo userCdo) {
// userStore.register();
}
public void modify(String userId, UserUdo userUdo) {
// userStore.modify();
}
public void remove(String userId) {
// userStore.remove();
}
}
UserService.java이다.
실제 서비스에서 DB에 저장하는 부분은 제외하였다. userStore를 통해 RDB나 NoSQL에 저장하는 부분을 구현하면 된다.
UserRdo
, UserCdo
, UserUdo
는 데이타를 직접적으로 전달하는 서비스 객체이다.
UserCdo.java
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UserCdo {
private String userId;
private String name;
private int age;
private String description;
}
UserRdo.java
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class UserRdo {
private String userId;
private String name;
private int age;
private String description;
public static UserRdo sample() {
return new UserRdo("hong", "홍길동", 20, "설명내용...");
}
}
UserUdo.java
@NoArgsConstructor
@Getter
public class UserUdo {
private String name;
private int age;
private String description;
}
controller 작성
위에서 정의한 API를 기반으로 UserController를 작성해 보겠다.
@RestController
@RequestMapping("users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping // 리스트
public List<UserRdo> findList() {
return userService.findList();
}
@GetMapping("{userId}") // 조회
public UserRdo find(@PathVariable String userId) {
return userService.find(userId);
}
@PostMapping // 등록
public void register(@RequestBody UserCdo userCdo) {
userService.register(userCdo);
}
@PutMapping("{userId}") // 수정
public void modify(@PathVariable String userId, @RequestBody UserUdo userUdo) {
userService.modify(userId, userUdo);
}
@DeleteMapping("{userId}") // 삭제
public void remove(@PathVariable String userId) {
userService.remove(userId);
}
}
테스트 구현
서비스 작성이 끝나면 해당 서비스를 문서화하기 위한 테스트 케이스를 만들어 보겠다.
UserControllerTest.java
@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class UserControllerTest {
private MockMvc mockMvc;
@MockBean
private UserService userService;
@BeforeEach
public void setUp(
WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
@Test
public void findList() throws Exception {
when(userService.findList())
.thenReturn(Arrays.asList(UserRdo.sample()));
this.mockMvc.perform(
get("/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("findList",
responseFields(
fieldWithPath("[].userId").description("아이디"),
fieldWithPath("[].name").description("이름"),
fieldWithPath("[].age").description("나이"),
fieldWithPath("[].description").description("설명")
)
));
}
@Test
public void find() throws Exception {
when(userService.find(anyString()))
.thenReturn(UserRdo.sample());
this.mockMvc.perform(
get("/users/{userId}", "hong")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("find",
pathParameters(
parameterWithName("userId").description("아이디")
),
responseFields(
fieldWithPath("userId").description("아이디"),
fieldWithPath("name").description("이름"),
fieldWithPath("age").description("나이"),
fieldWithPath("description").description("설명")
)
));
}
@Test
public void register() throws Exception {
this.mockMvc.perform(
post("/users")
.content("{\"userId\": \"hong\", \n\"name\": \"홍길동\", \n\"age\": 20, \n\"description\": \"설명\" }")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("register",
requestFields(
fieldWithPath("userId").type(JsonFieldType.STRING).description("아이디"),
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"),
fieldWithPath("description").type(JsonFieldType.STRING).description("설명")
)
));
}
@Test
public void modify() throws Exception {
this.mockMvc.perform(
put("/users/{userId}", "hong")
.content("{\n\"name\": \"홍길동\", \n\"age\": 20, \n\"description\": \"설명\" }")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("modify",
pathParameters(
parameterWithName("userId").description("아이디")
),
requestFields(
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("age").type(JsonFieldType.NUMBER).description("나이"),
fieldWithPath("description").type(JsonFieldType.STRING).description("설명")
)
));
}
@Test
public void remove() throws Exception {
this.mockMvc.perform(
delete("/users/{userId}", "hong")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andDo(document("remove",
pathParameters(
parameterWithName("userId").description("아이디")
)
));
}
}
mockMvc를 통해 해당 API를 호출하여 문서화를 위한 파라미터, 필드 등을 정의한다.
Request/Response Payloads, QueryParameters 등 기본적인 내용은 Spring 공식문서를 참고하기 바란다.
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#documenting-your-api-request-response-payloads
위의 테스트를 실행하기 위해서는 @SpringBootTest를 실행하여야 하기 때문에 test에 @SpringApplication이 필요하다.
test/하위에 Application.java를 만들어주자.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
adoc 파일 작성
위의 테스트 케이스를 통해서 snippets 파일을 만들어 주지만 문서화를 위한 adoc파일을 정의해 주어야 한다.
src/main/asciidoc하위에 adoc파일을 정의하자.
index.adoc
:doctype: book
= API Document
include::doc/user.adoc[leveloffset=+1]
user.adoc
= User
== findList
include::{snippets}/findList/curl-request.adoc[]
include::{snippets}/findList/http-response.adoc[]
include::{snippets}/findList/response-body.adoc[]
== find
include::{snippets}/find/curl-request.adoc[]
include::{snippets}/find/path-parameters.adoc[]
include::{snippets}/find/http-response.adoc[]
include::{snippets}/find/response-body.adoc[]
== register
include::{snippets}/register/curl-request.adoc[]
include::{snippets}/register/request-fields.adoc[]
== modify
include::{snippets}/modify/curl-request.adoc[]
include::{snippets}/modify/path-parameters.adoc[]
include::{snippets}/modify/request-fields.adoc[]
== remove
include::{snippets}/remove/curl-request.adoc[]
include::{snippets}/remove/path-parameters.adoc[]
실행
위에서 작성한 코드에서 만들어지는 요소는 2가지이다.
- snippets 파일 (mvn test)
- html 파일 : snippets파일을 통해 adoc에서 정의한 내용을 html로 만든다. (mvn prepare-package)
위와 같이 adoc 파일을 만들었으면 maven을 통해 문서를 만들어보자.
mvn prepare-package
위의 명령어를 실행하면 target 하위에 generated-snippets와 generated-docs가 생기는 것을 확인할 수 있다.
최종적으로는 user.html을 통해 api가 문서화가 된 것을 확인할 수 있다.
user.html