RestDocs 톺아보기Rest Docs 소개Rest Docs 아키텍처스니펫 예시(Test Case의 산출물)Asciidoctoc 설정API 테스트코드 작성하기document 사용할 수 있는 메서드
RestDocs 톺아보기
Rest Docs 소개
- 스프링 Rsct Docs의 목표는 Restful 서비스를 정확하고 읽기 편하게 문서화하는 것을 돕는 것을 목표로 합니다.
- 높은 수준의 문서화를 하는 것은 어렵고 작업에 잘 맞는 툴을 사용하는 데 있어서의 어려움을 편리하게 하는 하나의 방법으로 스프링은 RestDocs는 Asciidoctor를 사용한다.
- 아스키닥터는 평문을 처리하여 필요에 맞는 스타일과 레이어를 적용한 HTML을 만들어준다.
- Rest Docs는 Spring MVC Test를 위해 쓰여진 테스트를 통해 만들어진 코드 조각들을 사용한다.
- 이 테스트 기반의 접근법은 서비스에 대한 문서화의 정확도를 보장해주며 코드조각이 올바르지 않다면 결과물 생성에도 실패한다.
Rest Docs 아키텍처

- 테스트 케이스를 수행하면 산출물이 .adoc 파일로 기본적으로 세팅되는 경로일 경우 /build/generate-snippets 디렉토리에 생성됩니다.
- /src/docs/asciidoc 디렉토리에 /build/generate-snippets에 있는 adoc 파일을 include하여 문서를 생성할 수 있습니다.
- /build/generate-snippets의 .adoc 파일들은 API Request, Response에 대한 명세들만 있는 파일이다.
- /src/docs/asciidoc .adoc 파일들이 실제 사용자에게 html 파일로 변환되어 제공되는 API 문서 파일입니다.
- 즉 /src/docs/asciidoc .adoc에 APU 문서를 작성하고 필요한 API Request, Response Spec은 자동 생성된 /build/generate-snippets/ .adoc 파일들을 이용해 표현해줍니다.
- 이렇게 하면 향후 API Spec이 변경 되더라도, 문서를 수정하지 않아도 되는 장점이 있습니다.
- 이렇게 생성된 asciidoc 문서는 AsciidoctorTask를 통해 html로 만들 수 있습니다.
스니펫 예시(Test Case의 산출물)
- /build/generated-snippets 디렉토리 하위에 생성된다.
- Request, Response spec에 대한 정보 생성
- /src/docs/asciidoc/.adoc 파일에서 include 해서 사용한다.

Asciidoctoc 설정
// build.gradle plugins { ... id "org.asciidoctor.convert" version "1.5.9.2" // 추가 ... } dependencies { ... asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' // 추가 testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' // 추가 ... } // 밑에 전부 추가 ext { snippetsDir = file('build/generated-snippets') } test { useJUnitPlatform() outputs.dir snippetsDir } asciidoctor { inputs.dir snippetsDir dependsOn test } bootJar { dependsOn asciidoctor from ("${asciidoctor.outputDir}/html5") { into 'static/docs' } }
API 테스트코드 작성하기
@WebMvcTest(PostRestController.class) @AutoConfigureRestDocs class PostRestControllerTest { @Autowired private MockMvc mockMvc; @MockBean private PostService postService; @Autowired ObjectMapper objectMapper; @Test void 게시글_작성_테스트() throws Exception { User user = User.create("김형욱", 27, "산책"); Post post = Post.create(1L, user.getName(), "제목입니다.", "내용입니다내용입니다.", user); PostRequest.PostCreateDto postCreateDto = new PostRequest.PostCreateDto( "제목입니다.", "내용입니다내용입니다.", new MemberRequest.MemberCreateDto( "김형욱", 27, "산책" ) ); BDDMockito.given(postService.create(any(Post.class))) .willReturn(post); String response = objectMapper .writeValueAsString(new ApiResponse<Long>(1L, HttpStatus.CREATED)); // WHEN mockMvc.perform(post("/api/v1/posts") .contentType(MediaType.APPLICATION_JSON) .content( objectMapper.writeValueAsString(postCreateDto) )) .andExpect(content().string(response)) .andDo( document( "create-post", requestFields( fieldWithPath("title").type(JsonFieldType.STRING) .description("제목"), fieldWithPath("content").type(JsonFieldType.STRING) .description("내용"), fieldWithPath("memberDto").type(JsonFieldType.OBJECT) .description("작성자 정보"), fieldWithPath("memberDto.name").type(JsonFieldType.STRING) .description("작성자"), fieldWithPath("memberDto.age").type(JsonFieldType.NUMBER) .description("나이"), fieldWithPath("memberDto.hobby").type(JsonFieldType.STRING) .description("취미") ), responseFields( fieldWithPath("data").type(JsonFieldType.NUMBER) .description("게시글 아이디"), fieldWithPath("statusCode").type(JsonFieldType.STRING) .description("상태코드") ) ) ); } }
document 사용할 수 있는 메서드
- PathParameters
- API의 Path Parameter에 대한 스니펫을 생성한다.
document( "create-member", pathParameters(parameterWithName("memberId").description("맴버 아이디")), // Path의 파라미터 정보에 대한 스니펫을 생성해줍니다. );
- requestHeader
- API의 Request Header에 대한 스니펫을 생성합니다.
document( "create-member", // API의 Request Header에 대한 스니펫을 생성해줍니다. requestHeaders(headerWithName("token").description("토큰 정보")), );
- fieldWithPath
- API의 Request, Response 스니펫을 구성하는 요소를 정의하는 기본적인 메서드이다.
- fieldWithPath(”key”) 형태로 API Request, Response 내의 요소를 정의 할 수 있다.
- 추가 속성
- description(내용) : 요청, 응답 내의 요소 내용을 정의한다.
- type() : 내용 요소의 타입을 정의한다.
- optional() : 내용 요소의 필수값 여부를 정의한다.
requestFields( fieldWithPath("id").type(JsonFieldType.NUMBER) .description("아이디").optional(), fieldWithPath("name").type(JsonFieldType.STRING) .description("이름"), fieldWithPath("age").type(JsonFieldType.NUMBER) .description("나이") ), responseFields( fieldWithPath("id").type(JsonFieldType.NUMBER) .description("아이디"), fieldWithPath("name").type(JsonFieldType.STRING) .description("이름"), fieldWithPath("age").type(JsonFieldType.NUMBER) .description("나이") )
- subSectionWithPath
- API 요청, 응답 스니펫을 구성하는 요소를 정의하는 메서드이다.
- 요소의 하위를 선언하고 싶거나 가변적인 요청, 응답이 오는 경우 사용하기 좋다.
- 에러 하위에 대한 정보는 문서화 하지 않을 경우
resounseFields(*common()).and(subSEctionWithPath("errors") .type(JsonFieldType.ARRAY).description("에러 정보")
- responseField().and()
- response에 대한 필드 정보가 많을 경우 오히려 가독성을 있다. 즉 필드 스니펫에 대한 재사용성이 떨어질 수 있다. 그럴 경우 and() 메서드를 사용하면 좋다.
- 아래의 예시처럼 key의 레벨이 2인 Json이 오는 경우 사용할 수 있다.
{ "title" : "제목", "content" : "내용", "member" : { "name" : "작성자" } }
responseFields(*common()).andWithPrefix("data.", *user())
- beneathPath
- API를 사용하다 보면 특정 Field의 명세만 별도로 추출한 문서가 필요할 수 있다.
- 이 경우 beneathPath(’data-path’) 메서드를 사용하면 특정 data-path에 대한 필드를 별도의 스니펫으로 생성해준다.
responseFields( beneathPath("data").withSubSectionId("user"), *user(), subsectionWithPath("roles").description("유저 권한"); )
추가기능 - custom 필드
인터페이스 정적 메서드
public class ErrorField { String status; int code; String message; List<FieldError> fieldErrors; } public interface RestDocsUtils { static ResponseFieldsSnippet getErrorResponseFieldsWithFieldErrors() { return responseFields( fieldWithPath("status").description("에러 상태"), fieldWithPath("code").description("에러 코드"), fieldWithPath("message").description("에러 메세지"), subsectionWithPath("errors").description("필드 에러").optional() ); } }
