© 2025 Sejin Cha. All rights reserved.
Built with Next.js, deployed on Vercel
REST DOCS 사용 예시 참고
우아한 형제들 Spring RestDocs에 날개를 — adoc 파일의 table of content, 파일 폰트, highlight 설정 등의 내용
Spring RestDocs 적용 및 최적화 — print 공통 처리, generated snippet의 네이밍 설정 등 Bean으로 설정하여 계속 반복적 작업 줄여주는 내용 포함되어 있음
의존성 추가
이용방법 1. 테스트 코드 작성 with document( ) 테스트 클래스에 @AutoConfigureRestDocs 적용 이렇게 해서 테스트 할 시, target/generated-snippets/{order-save}(document 메서드 안에 명시하는 이름)에 해당하는 asciidocs 파일들이 생성됨 해당 ascii docs들을 이용하여 index.adoc을 작성함 Parameterized Output Directories 2. 생성된 adoc파일들을 이용하여 index.adoc을 작성 asciidoc 문법 섹션 제목
[[]]
: 이건 그냥 코멘트 용도로 사용하는 듯함목록 추가, 속성 추가 3. 해당 index.adoc을 html 파일로 변경 자동으로 asciidoc 이 찾는 위치 위 링크 참고하면 plugin 설정하는 방법이 있는데 plugin을 이용하면 알아서 html 파일 생성해줌 조금 아래에 살펴보면 Packaging the documentation도 있음. 배포할 때 static content로 포함시켜서 배포하기 요청, 응답 명세 작성 시 사용하는 메서드 RestDocs 사용 클래스
RequestDocumentation
: queryParam, pathVariable 에 대해서 사용하는 클래스요청과 응답 커스터마이징 request와 response에 대해서 preprocesor
를 이용하여 문서화 하기 전에 변형을 가함 모든 테스트에 대해 동일한 preprocessor 를 적용하기 MockMvcBuilders에서 alwaysDo(restDocs)
를 적용시켜두면, mockMvc로 호출만 해도, 아래 adoc파일이 자동으로 생성됨 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
}
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("서버시간")
)));
}
}
: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은 한번에 가져올 수 있음 = 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[]
// 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 명세는 지원하지 않음
@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를 자동 실행하도록 등록해줌 tasks.named('bootJar') {
dependsOn asciidoctor
from ("build/docs/asciidoc") {
into "static/docs"
}
}