Spring

[Spring] RestDocs 기반으로 API 문서 작성해보기

Unan 2024. 3. 16. 04:15
반응형

보통 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에 물어봤다.

 

  1. :doctype: book: 문서의 유형을 지정합니다. 여기서는 book으로 설정되어 있습니다. 다른 유형으로는 article, manpage, inline 등이 있습니다.
  2. :icons: font: 아이콘 세트를 지정합니다. 여기서는 font로 설정되어 있습니다. 이는 문서에서 글꼴 기반의 아이콘을 사용하겠다는 의미입니다. 다른 옵션으로는 image가 있습니다.
  3. :source-highlighter: highlightjs: 소스 코드 하이라이팅에 사용할 하이라이터를 지정합니다. 여기서는 highlightjs로 설정되어 있습니다. 다른 옵션으로는 coderay, pygments, prettify 등이 있습니다.
  4. :toc: left: 목차(Table of Contents)의 위치를 지정합니다. 여기서는 left로 설정되어 있어 목차가 문서의 왼쪽에 표시됩니다. 다른 옵션으로는 right, top, bottom, macro 등이 있습니다.
  5. :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가 되지 않는 문제가 있었다..

 

관련하여 이후에  해결해볼예정 ..

반응형