보통 Swagger를 작성하여 API 문서를 많이 작성했는데, Controller의 Test를 작성하고 작성한 Test를 기반으로 API 문서를 만들어주는 RestDocs에 대해서 공부해보고 API 문서를 작성해봤다.
https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/
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
해당 공식문서 기반으로 RestDocs를 적용했다. RestDocs관련 설정을 build.gradle에 추가해준다.

이후에 각 Controller Test에서 RestDocs를 사용하여 TestCode를 작성하기 때문에 관련 설정을 미리 해둔 class를 만들고, 해당 class를 상속받아 TestCode를 작성하는 형태로 개발을 진행했다.
RestDocumentationExtension 은 BeforeEachCallback, AfterEachCallback, ParameterResolver 를 구현하고 있다.
public class RestDocumentationExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver {
private final String outputDirectory;
public RestDocumentationExtension() {
this((String)null);
}
public RestDocumentationExtension(String outputDirectory) {
this.outputDirectory = outputDirectory;
}
public void beforeEach(ExtensionContext context) throws Exception {
this.getDelegate(context).beforeTest(context.getRequiredTestClass(), context.getRequiredTestMethod().getName());
}
public void afterEach(ExtensionContext context) throws Exception {
this.getDelegate(context).afterTest();
}
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return this.isTestMethodContext(extensionContext) ? RestDocumentationContextProvider.class.isAssignableFrom(parameterContext.getParameter().getType()) : false;
}
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context) {
return () -> {
return this.getDelegate(context).beforeOperation();
};
}
private boolean isTestMethodContext(ExtensionContext context) {
return context.getTestClass().isPresent() && context.getTestMethod().isPresent();
}
private ManualRestDocumentation getDelegate(ExtensionContext context) {
ExtensionContext.Namespace namespace = Namespace.create(new Object[]{this.getClass(), context.getUniqueId()});
return (ManualRestDocumentation)context.getStore(namespace).getOrComputeIfAbsent(ManualRestDocumentation.class, this::createManualRestDocumentation, ManualRestDocumentation.class);
}
private ManualRestDocumentation createManualRestDocumentation(Class<ManualRestDocumentation> key) {
return this.outputDirectory != null ? new ManualRestDocumentation(this.outputDirectory) : new ManualRestDocumentation();
}
}
해당 class를 설정 클래스 위에 @ExtendWith(RestDocumentationExtension.class) 형태로 사용할 수 있다.
MockMvcBuilders는 두가지 Builder를 호출할 수 있다.
public final class MockMvcBuilders {
private MockMvcBuilders() {
}
public static DefaultMockMvcBuilder webAppContextSetup(WebApplicationContext context) {
return new DefaultMockMvcBuilder(context);
}
public static StandaloneMockMvcBuilder standaloneSetup(Object... controllers) {
return new StandaloneMockMvcBuilder(controllers);
}
}
이 때 standaloneSetup()에 controller를 parameter로 받아서 MockMvc를 생성해준다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class RestDocsEnv {
protected MockMvc mockMvc;
protected ObjectMapper objectMapper = new ObjectMapper();
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders
.standaloneSetup(initializeController())
.apply(documentationConfiguration(provider))
.build();
}
protected abstract Object initializeController();
}
setUp method위에 @BeforeEach가 있기 때문에 각 Controller에서 RestDocsEnv를 상속한 뒤에 추앗 method intializeController()를 주입해주면 된다.
Test할 class에서 아래와 같이 구현했다.
public class QuestionControllerTest extends RestDocsEnv {
private final QuestionService questionService = mock(QuestionService.class);
@Override
protected Object initializeController() {
return new QuestionController(questionService);
}
@Test
@DisplayName("질문 목록을 조회할 수 있다.")
void getQuestions() throws Exception {
// given
List<GetQuestionResponse> response = List.of(
new GetQuestionResponse(1L, "질문1"),
new GetQuestionResponse(2L, "질문2")
);
BDDMockito.given(questionService.getQuestionResponseList())
.willReturn(response);
// when
ResultActions result = mockMvc.perform(get("/question"));
// then
result.andExpect(status().isOk())
.andDo(print())
.andDo(document("get-questions",
responseFields(
fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"),
fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메시지"),
fieldWithPath("data[].questionId").type(JsonFieldType.NUMBER).description("질문 식별자").optional(),
fieldWithPath("data[].questionContent").type(JsonFieldType.STRING).description("질문 내용").optional()
)
));
}
}
이렇게 작성한 Test가 통과하게 되면, 설정한 output directory에 *.adoc 파일이 만들어진다.

해당 .adoc 파일을 활용하여 문서를 작성할 수 있다. src/docs/ascii/index.doc 파일을 만들고 아래와 같이 작성할 수 있다.
ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
= Test 프로젝트 REST API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels:
[[Question-API]]
= Question API
== Get Questions
==== HTTP Request
include::{snippets}/get-questions/http-request.adoc[]
==== Request Example
include::{snippets}/get-questions/curl-request.adoc[]
==== HTTP Response
include::{snippets}/get-questions/http-response.adoc[]
include::{snippets}/get-questions/response-fields.adoc[]
각 설정이 의미하는 것은 생성형 AI에 물어봤다.
- :doctype: book: 문서의 유형을 지정합니다. 여기서는 book으로 설정되어 있습니다. 다른 유형으로는 article, manpage, inline 등이 있습니다.
- :icons: font: 아이콘 세트를 지정합니다. 여기서는 font로 설정되어 있습니다. 이는 문서에서 글꼴 기반의 아이콘을 사용하겠다는 의미입니다. 다른 옵션으로는 image가 있습니다.
- :source-highlighter: highlightjs: 소스 코드 하이라이팅에 사용할 하이라이터를 지정합니다. 여기서는 highlightjs로 설정되어 있습니다. 다른 옵션으로는 coderay, pygments, prettify 등이 있습니다.
- :toc: left: 목차(Table of Contents)의 위치를 지정합니다. 여기서는 left로 설정되어 있어 목차가 문서의 왼쪽에 표시됩니다. 다른 옵션으로는 right, top, bottom, macro 등이 있습니다.
- :toclevels: 2: 목차에 포함할 제목의 수준을 지정합니다. 여기서는 값이 비어 있어 기본값인 2가 사용됩니다. 이는 == 수준까지의 제목만 목차에 포함된다는 의미입니다. 값을 3으로 설정하면 === 수준까지의 제목이 목차에 포함됩니다
이렇게 작성한 뒤에 AsciiDoc 플러그인을 추가해주면 미리보기도 확인할 수 있다.

이렇게 작성을 마친 뒤에, 어플리케이션을 실행하고 HOST/docs/index.html로 접속하여 API 문서를 확인할 수 있다.

장점이 Request, Response, Response Field에 대한 설명을 바로 확인할 수 있고 curl request example을 쉽게 표시해줄 수 있는것이 매우 좋다고 생각했다.
실제로 API Test를 할 때 curl로 하는 경우가 많기 때문에 그대로 복사해서 사용하면 되므로 편하다고 생각했다..
하지만 확실히 Swagger 보다는 UI가 깔끔하지는 않은 것 같다 ㅋㅋ 빠르게 개발할 때는 Test는 WebMvcTest로 대체하고, Swagger를 사용하는 방법이 있는 것 같다.
적용해보고 ..
RestDocs를 사용하면서 API Test 짜는 습관을 가져보면 좋을 것 같아서 앞으로 RestDocs를 적극 활용하려고 한다..!
API 문서가 많아지는 경우에 파일을 분리하고, index.adoc에서 include를 이용하여 다른 문서파일을 갖고올 수 있는 것으로 확인했는데, 실제 이와 같이 작업하고 애플리케이션을 실행하여 확인해보면 경로 문제로 include가 되지 않는 문제가 있었다..
관련하여 이후에 해결해볼예정 ..
'Spring' 카테고리의 다른 글
| [Spring] JPA ID 설계 전략 (0) | 2024.06.13 |
|---|---|
| [Smeem] Presentation Layer <-> Application Layer DTO 리팩토링 (0) | 2024.03.22 |
| [Study] Test Code Study 정리 (1) (1) | 2024.02.27 |
| [Spring] AWS EC2에 Spring Boot Project 배포하기. (0) | 2023.06.05 |
| [Spring Boot] Spring Boot로 HTTP API 설계하기 (2) | 2023.05.10 |