HomeAboutMeBlogGuest
© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
🤩
개발
/
Spring
Spring
/
REST DOCS
REST DOCS
REST DOCS

REST DOCS

[사용방법 참고]
[Spring.io Guide] Creating API Documentation with Restdocs
[ Spring Docs ] Spring RestDocs, Setting up your tests
사용 예시 참고
우아한 형제들 Spring RestDocs 적용
우아한 형제들 Spring RestDocs에 날개를 — adoc 파일의 table of content, 파일 폰트, highlight 설정 등의 내용
Spring RestDocs 적용 및 최적화 — print 공통 처리, generated snippet의 네이밍 설정 등 Bean으로 설정하여 계속 반복적 작업 줄여주는 내용 포함되어 있음
Swagger와 Rest DOCS 연동
 
의존성 추가이용방법1. 테스트 코드 작성 with document( ) 2. 생성된 adoc파일들을 이용하여 index.adoc을 작성asciidoc 문법목록 추가, 속성 추가3. 해당 index.adoc을 html 파일로 변경요청, 응답 명세 작성 시 사용하는 메서드요청과 응답 커스터마이징모든 테스트에 대해 동일한 preprocessor 를 적용하기REST DOCS 생성 파일 jar에 포함시키기

의존성 추가

<dependency> <groupId>org.springframework.restdocs</groupId> <artifactId>spring-restdocs-mockmvc</artifactId> <scope>test</scope> </dependency>
plugins { id "org.asciidoctor.jvm.convert" version "3.3.2" } configurations { asciidoctorExt } dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' } ext { snippetsDir = file('build/generated-snippets') } test { outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir configurations 'asciidoctorExt' dependsOn test }

이용방법

1. 테스트 코드 작성 with document( )

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.payload.PayloadDocumentation.*; 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; @AutoConfigureMockMvc @AutoConfigureRestDocs @SpringBootTest class OrderControllerTest { @Test void createOrderTest() throws Exception { mvc.perform(post("/orders") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(orderDtoData))) .andExpect(status().isOk()) .andDo(print()) .andDo(document("order-save", requestFields( fieldWithPath("uuid").type(JsonFieldType.STRING).description("UUID"), fieldWithPath("orderDatetime").type(JsonFieldType.STRING).description("orderDatetime"), fieldWithPath("orderStatus").type(JsonFieldType.STRING).description("orderStatus"), fieldWithPath("memo").type(JsonFieldType.STRING).description("memo"), fieldWithPath("memberDto").type(JsonFieldType.OBJECT).description("memberDto"), fieldWithPath("memberDto.id").type(JsonFieldType.NUMBER).description("memberDto.id"), fieldWithPath("memberDto.name").type(JsonFieldType.STRING).description("memberDto.name"), fieldWithPath("memberDto.nickName").type(JsonFieldType.STRING).description("memberDto.nickName"), fieldWithPath("memberDto.age").type(JsonFieldType.NUMBER).description("memberDto.age"), fieldWithPath("memberDto.address").type(JsonFieldType.STRING).description("memberDto.address"), fieldWithPath("memberDto.description").type(JsonFieldType.STRING).description("memberDto.description"), fieldWithPath("orderItemDtos").type(JsonFieldType.ARRAY).description("orderItemDtos"), fieldWithPath("orderItemDtos[].id").type(JsonFieldType.NUMBER).description("orderItemDtos[].id"), fieldWithPath("orderItemDtos[].quantity").type(JsonFieldType.NUMBER).description("orderItemDtos[].quantity"), fieldWithPath("orderItemDtos[].price").type(JsonFieldType.NUMBER).description("orderItemDtos[].price"), fieldWithPath("orderItemDtos[].itemDto").type(JsonFieldType.OBJECT).description("orderItemDtos[].itemDto"), fieldWithPath("orderItemDtos[].itemDto.id").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.id"), fieldWithPath("orderItemDtos[].itemDto.type").type(JsonFieldType.STRING).description("orderItemDtos[].itemDto.type"), fieldWithPath("orderItemDtos[].itemDto.price").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.price"), fieldWithPath("orderItemDtos[].itemDto.stockQuantity").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.stockQuantity"), fieldWithPath("orderItemDtos[].itemDto.power").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.power"), fieldWithPath("orderItemDtos[].itemDto.chef").type(JsonFieldType.NULL).description("orderItemDtos[].itemDto.chef"), fieldWithPath("orderItemDtos[].itemDto.width").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.width"), fieldWithPath("orderItemDtos[].itemDto.height").type(JsonFieldType.NUMBER).description("orderItemDtos[].itemDto.height") ), responseFields( fieldWithPath("statusCode").type(JsonFieldType.NUMBER).description("상태코드"), fieldWithPath("data").type(JsonFieldType.STRING).description("데이터"), fieldWithPath("serverDateTime").type(JsonFieldType.STRING).description("서버시간") ))); } }
  • 테스트 클래스에 @AutoConfigureRestDocs 적용
  • 이렇게 해서 테스트 할 시, target/generated-snippets/{order-save}(document 메서드 안에 명시하는 이름)에 해당하는 asciidocs 파일들이 생성됨
  • 해당 ascii docs들을 이용하여 index.adoc을 작성함
Parameterized Output Directories
notion image

2. 생성된 adoc파일들을 이용하여 index.adoc을 작성

빌드툴 별로 asciidoc 소스파일들과 plugin을 사용해서 생성한 html의 위치가 다름 [RESTDOCS 문서 참고]
빌드툴 별로 asciidoc 소스파일들과 plugin을 사용해서 생성한 html의 위치가 다름 [RESTDOCS 문서 참고]
:hardbreaks: ifndef::snippets[] :snippets: ../../../target/generated-snippets endif::[] == 주문 === 주문 생성 === /orders/{uuid} .Request include::{snippets}/order-save/http-request.adoc[] include::{snippets}/order-save/request-fields.adoc[] operation::user-controller-test/로그인_성공[snippets='request-fields,response-fields'] .Response include::{snippets}/order-save/http-response.adoc[] include::{snippets}/order-save/response-fields.adoc[]
include, operation 명령어를 이용하여 생성된 snippet을 이용할 수 있음. include는 하나씩 가져오는데 반해, operation은 한번에 가져올 수 있음

asciidoc 문법

참고 https://asciidoctor.org/docs/asciidoc-writers-guide/#document-attributes
섹션 제목
= Document Title (Level 0) == Level 1 Section === Level 2 Section ==== Level 3 Section ===== Level 4 Section ====== Level 5 Section
  • [[]] : 이건 그냥 코멘트 용도로 사용하는 듯함
  • ‘’’ : 라인 생성

목록 추가, 속성 추가

[[REALWORLD]] = Realworld :doctype: book :icons: font :source-highlighter: highlightjs // 문서에 표기되는 코드들의 하이라이팅을 highlightjs를 사용 :toc: left. // table of contents. 왼쪽에 목차 위치 :toclevels: 2 // 몇 단계 레벨까지 나타낼지 :sectlinks: :docinfo: shared-head include::API/user-api.adoc[]

3. 해당 index.adoc을 html 파일로 변경

[Spring Rest Docs 문서] Build Configuration
자동으로 asciidoc 이 찾는 위치
자동으로 asciidoc 이 찾는 위치
  • 위 링크 참고하면 plugin 설정하는 방법이 있는데 plugin을 이용하면 알아서 html 파일 생성해줌
  • 조금 아래에 살펴보면 Packaging the documentation도 있음. 배포할 때 static content로 포함시켜서 배포하기

요청, 응답 명세 작성 시 사용하는 메서드

[ Spring Docs] REST DOCS — Documenting your API
// PathParameter에 대한 명세( pathParameters를 쓸 때는 RestDocumentationRequestBuilders // 를 이용하여 호출해야함! // org.springframework.restdocs.request.RequestDocumentation mockMvc.perform( RestDocumentationRequestBuilders.delete(ENDPOINT_URL_PREFIX + "{userCategoryId}", userCategoryId)) .andExpect(status().isNoContent()) .andDo( MockMvcRestDocumentation.document("aa", pathParameters(parameterWithName("userCategoryId").description("카테고리 아이디")) ) ); // requestBody, responseBody mockMvc.perform(patch(ENDPOINT_URL_PREFIX + "/{userCategoryId}", userCategoryId) .content(objectMapper.writeValueAsString(body)) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andDo( restDocs.document( PayloadDocumentation.requestFields( fieldWithPath("name").type(JsonFieldType.STRING).description("업데이트할 카테고리 이름") ), responseFields( fieldWithPath("name").type(JsonFieldType.STRING).description("업데이트 된 카테고리 이름") ) ) ); // QueryParameter 명세 mockMvc.perform(RestDocumentationRequestBuilders.get(ENDPOINT_URL_PREFIX).queryParam("kind", categoryType)) .andExpect(status().isOk()) .andDo( restDocs.document( RequestDocumentation.requestParameters(( RequestDocumentation.parameterWithName("kind").description("카테고리 종류") ), responseFields( fieldWithPath("categories[].id").type(JsonFieldType.NUMBER).description("카테고리 아이디"), fieldWithPath("categories[].name").type(JsonFieldType.STRING).description("카테고리 이름"), fieldWithPath("categories[].categoryType").type(JsonFieldType.STRING).description("카테고리 종류") ) ) ); // binary payload에 대한 API 명세는 지원하지 않음
RestDocs 사용 클래스
  • RequestDocumentation : queryParam, pathVariable 에 대해서 사용하는 클래스
  • PayloadDocumentation
  • HeaderDocumentation

요청과 응답 커스터마이징

[ Spring Docs ] REST DOCS — Customizing requests and responses
  • request와 response에 대해서 preprocesor를 이용하여 문서화 하기 전에 변형을 가함

모든 테스트에 대해 동일한 preprocessor 를 적용하기

Spring RestDocs 적용 및 최적화
@TestConfiguration public class RestDocsConfig { @Bean public RestDocumentationResultHandler resultHandler() { return MockMvcRestDocumentation.document( "{class-name}/{method-name}", Preprocessors.preprocessRequest(Preprocessors.prettyPrint()), Preprocessors.preprocessResponse(Preprocessors.prettyPrint()) ); } }
RestDocsConfig.java — rest docs를 위한 preprocessor를 정의
@SpringBootTest @Import(RestDocsConfig.class) @ActiveProfiles("test") @ExtendWith(RestDocumentationExtension.class) public abstract class AbstractControllerTest { @Autowired protected RestDocumentationResultHandler restDocs; protected MockMvc mvc; protected final ObjectMapper objectMapper = new ObjectMapper(); protected final ResponseFieldsSnippet commonResponse = PayloadDocumentation.responseFields( PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), PayloadDocumentation.fieldWithPath("message").type(JsonFieldType.STRING).description("상태 메시지"), PayloadDocumentation.fieldWithPath("data[]").type(JsonFieldType.ARRAY).description("응답 본문")); @BeforeEach public void setup(RestDocumentationContextProvider provider, WebApplicationContext context) { this.mvc = MockMvcBuilders.webAppContextSetup(context) .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) .apply(SecurityMockMvcConfigurers.springSecurity()) .alwaysDo(restDocs) .addFilter(new CharacterEncodingFilter("UTF-8", true)) .build(); }
setup 메서드에서 this.mvc에 MockMvcBuilders를 활용해, RestDocs의 preprocessor를 자동 실행하도록 등록해줌
[ MockMvcBuilders 사용시 유의점 ] @AutoconfigureMockMvc @SpringBootTest : 애플리케이션을 실행하지 않고, Spring이 HTTP request를 handle 하고 controller에 넘겨주는 것 까지만 테스트 하는 방법 : SecurityMockMvcConfigurers.springSecurity() 적용 안하면 SecurityFilter들이 제외됨
MockMvcBuilders에서 alwaysDo(restDocs)를 적용시켜두면, mockMvc로 호출만 해도, 아래 adoc파일이 자동으로 생성됨
notion image

REST DOCS 생성 파일 jar에 포함시키기

tasks.named('bootJar') { dependsOn asciidoctor from ("build/docs/asciidoc") { into "static/docs" } }