도입
API 명세서는 각 API가 어떻게 동작하는지 설명하는 문서로, 프론트엔드와 서버 간 협업에서 필수적이다.
나는 기존 프로젝트들에서 명세서 작성 도구로 Swagger를 사용했었다.

그러다 최근에 Spring Rest Docs로도 API 명세서를 작성할 수 있음을 알게 되었다.
Spring Rest Docs는 스프링 프레임워크에서 제공하는 API 문서 자동화 도구이다.
Swagger VS Rest Docs
Swagger와 Rest Docs는 각각 장단점이 존재한다.
Swagger는 적용이 쉽고, 해당 문서에서 API 호출 테스트를 직접 해볼 수 있다.
그러나 프로덕션 코드에 Swagger 문서 관련 코드가 포함되어 가독성이 떨어진다.
Rest Docs는 Swagger에 비해 적용이 어렵지만 프로덕션 코드에 비침투적이다.
또한, 테스트가 성공해야 문서가 생성되기 때문에 API 명세서를 신뢰할 수 있다.
이를 표로 정리하면 다음과 같다.

나는 협업에서 API 문서의 신뢰도가 중요하다고 생각했고, 이에 Spring Rest Docs를 적용했다.
동작 원리
테스트 코드가 성공하면 해당 테스트에 대한 asciidoc snippet이 생성된다.
직접 작성한 index.adoc 파일에 이 snippet들을 추가하여 합칠 수 있다.
최종적으로 Asciidoctor가 이 index.adoc을 index.html로 변환해 준다.
아래 구현한 코드를 보면 동작 원리가 더 잘 이해될 것이다.
build.gradle 설정
Rest Docs를 사용하기 위해 build.gradle에 아래 코드들을 작성한다.
먼저 asciidoctor 플러그인을 추가해 준다.
plugins {
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
asciidoctor와 mockMvc 의존성을 추가해 주었다.
asciidoctorExt는 build/generated-snippets에 있는 .adoc 파일을 .html로 변환해 준다.
mockMvc는 restDocs 생성을 위한 테스트에서 사용된다.
configurations {
asciidoctorExt
}
dependencies {
asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
테스트 성공 후 생성되는 snippet을 snippetsDir에 들어가도록 했다.
dependsOn을 사용해 test가 끝난 후 asciidoctor가 실행되도록 하였다.
ext { // 전역 변수 선언
snippetsDir = file('build/generated-snippets')
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
configurations 'asciidoctorExt'
dependsOn test
}
asciidoctor에 의해 html이 생성될 것이다.
빌드 시 jar 파일이 해당 html을 가지고 있도록 설정했다.
bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
테스트 코드 작성
공통 환경 정의
컨트롤러 테스트를 작성하려면 mockMvc가 필요하다.
mockMvc를 생성하는 코드를 추상 클래스 내에 정의해 여러 컨트롤러 테스트에서 상속받아 사용할 수 있도록 하였다.
@ExtendWith(RestDocumentationExtension.class)
public abstract class RestDocsSupport {
protected MockMvc mockMvc;
@BeforeEach
void setUp(RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.standaloneSetup(initController())
.apply(documentationConfiguration(provider)) //문서 만드는 설정으로 provider 주입
.build();
}
protected abstract Object initController();
}
standaloneSetup()으로 MockMvc를 생성하면, 스프링 의존성 없이 각 컨트롤러를 독립적으로 테스트할 수 있다.
해당 메서드 파라미터로는 controller가 들어간다.
추상 메서드 initController()를 통해 하위 구현체에서 controller를 직접 주입하였다.
(참고)
webAppContextSetup()에 WebApplicationContext를 넣어 MockMvc를 생성할 수 있다.
하지만 context는 Spring 의존성을 필요로 하기 때문에 @SpringBootTest가 필요하다.
문서 작성 시에도 스프링 서버를 띄운다면 문서 배포 속도가 늦어질 것이다.
@ExtendWith(RestDocumentationExtension.class)
@SpringBootTest
public abstract class RestDocsSupport {
protected MockMvc mockMvc;
@BeforeEach
void setUp(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider provider) {
this.mockMvc = MockMvcBuilders.webAppcontextSetUp(webApplicationContext)
.apply(documentationConfiguration(provider)) //문서 만드는 설정으로 provider 주입
.build();
}
}
두 방식은 스프링 컨텍스트를 넣어 컨트롤러를 자동 주입할 것인지 아니면 스프링 의존성 없이 컨트롤러를 수동으로 주입할지 차이라고 보면 될 것 같다.
컨트롤러 테스트 작성
위에서 정의한 RestDocsSupport를 상속받는 ControllerTest 클래스를 생성하였다.
mocking한 Service를 Controller에 주입해 메서드로 반환함으로써 부모 클래스에서 사용할 수 있도록 했다.
class AlbumControllerDocsTest extends RestDocsSupport {
private final AlbumService albumService = mock(AlbumService.class);
@Override
protected Object initController() {
return new AlbumController(albumService);
}
request와 response의 필드들을 각각 정의해 주었다.
prettyPrint()는 한 줄로 되어 있는 json Object를 여러 줄에 걸쳐 출력할 수 있게 해 준다.
@DisplayName("앨범 생성 API")
@Test
void createAlbum() throws Exception {
CreateAlbumRequest requestDto = createAlbumRequest();
CreateAlbumResponse responseDto = createAlbumRes();
given(albumService.createAlbum(requestDto)).willReturn(responseDto);
mockMvc.perform(post("/albums")
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isOk())
.andDo(document("album-create",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
requestFields(
fieldWithPath("title").type(JsonFieldType.STRING)
.description("제목"),
fieldWithPath("artist").type(JsonFieldType.STRING)
.description("아티스트"),
fieldWithPath("category").type(JsonFieldType.STRING)
.description("카테고리"),
fieldWithPath("imgUrl").type(JsonFieldType.STRING)
.description("이미지 url 주소"),
fieldWithPath("releaseDate").type(JsonFieldType.STRING)
.description("발매일"),
fieldWithPath("price").type(JsonFieldType.NUMBER)
.description("가격")
),
responseFields(
fieldWithPath("createdAlbumId").type(JsonFieldType.NUMBER)
.description("생성된 앨범 아이디")
)
));
테스트를 성공하면 build 폴더 하위에 snippet들이 생성된다.

이렇게 작성된 snippet들을 index.adoc에 합쳐줄 수 있다.
ifndef::snippets[]
:snippets: ../../build/generated-snippets
endif::[]
= Album Market REST API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
[[Album-API]]
== Album API
[[product-create]]
=== 신규 상품 등록
==== HTTP Request
include::{snippets}/album-create/http-request.adoc[]
include::{snippets}/album-create/request-fields.adoc[]
==== HTTP Response
include::{snippets}/album-create/http-response.adoc[]
include::{snippets}/album-create/response-fields.adoc[]
각 API의 모든 snippets들을 한 index.adoc에 작성한다면 코드가 길어질 수 있다.
이를 보완하기 위해 도메인 API별로 별도의 adoc파일를 생성해 불러올 수 있다.
아래는 album.adoc 파일이다.
[[product-create]]
=== 신규 상품 등록
==== HTTP Request
include::{snippets}/album-create/http-request.adoc[]
include::{snippets}/album-create/request-fields.adoc[]
==== HTTP Response
include::{snippets}/album-create/http-response.adoc[]
include::{snippets}/album-create/response-fields.adoc[]
해당 파일을 index.adoc에 포함시킬 수 있다.
...
[[Album-API]]
== Album API
include::api/album/album.adoc[]
빌드를 해보면 docs 폴더 내에 asciiDoctor가 생성한 index.html이 생긴다.

문서 웹 브라우저에서 실행
빌드 시 생성된 jar 파일을 실행하자.

이후 http://localhost:8080/docs/index.html로 접속하면 rest docs로 생성한 명세서가 잘 보인다.


Reference
Practical Testing: 실용적인 테스트 가이드 강의 - 인프런
이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강
www.inflearn.com
Spring REST Docs를 사용한 API 문서 자동화
API 문서 자동화 백엔드와 프론트엔드 개발자 사이의 원활한 협업을 위해서는 REST API 명세에 대한 문서화가 잘 되어있어야 한다. 구글 독스, 스프레드 시트, 위키, 노션 등을 사용해서 직접 API 명
hudi.blog