[Spring] Spring에서 API 문서 자동화하기 (2) - Spring REST Docs

Jungwoong (David) Yoon·2023년 8월 8일

Spring

목록 보기
3/4

Spring에서 API 문서 자동화하기 (1) - Swagger

이전 글에서는 Swagger에 대해 알아보고 현재 진행 중인 프로젝트에 간단하게 적용해봤다. 이번에는 Spring REST Docs를 사용해보자.


Spring REST Docs

특징


Spring REST Docs는 Swagger와는 다르게 코드에 문서화를 위한 별도의 코드를 추가하지 않아도 된다.

대신, 테스트 코드를 작성하며 API를 명세할 수 있다. 미리 정의한 테스트가 실행되고 해당 테스트가 성공하면, 그 테스트에 대한 markdown/asciidoc snippet이 생성된다.

RestAssured vs. MockMvc


Spring REST Docs를 위해 사용할 수 있는 테스트는 대표적으로 RestAssured와 MockMvc가 있다.

RestAssured

RestAssured 는 별도의 구성을 하지 않는 이상 @SpringBootTest 와 함께 사용해야 한다.

@SpringBootTest 는 스프링의 전체 빈을 컨텍스트에 모두 띄워서 테스트 환경을 구동한다. 즉, 어플리케이션이 동작하는 실제 환경과 동일한 환경에서 테스트를 진행하고 싶을 때 RestAssured 를 사용한다.

때문에 @SpringBootTest 를 사용하는 RestAssured 는 느리고, 비용이 많이 든다.

MockMvc

반면 MockMvc는 @SpringBootTest 와 함께 사용할 수도 있고, @WebMvcTest 와 함께 사용할 수도 있다.

@WebMvcTest@SpringBootTest 와 다르게 프레젠테이션 레이어(@Controller , @ControllerAdvice , Servlet , 등 전반적인 MVC를 담당하는 layer) 의 빈들만 로드하고 나머지 계층은 Mocking을 한다. 이렇게 독립적으로 하나의 계층만을 테스트 하는 기법을 슬라이스 테스트라고 한다.

일반적으로 문서화를 위한 컨트롤러 테스트를 작성할 때에는 컨트롤러 이외의 계층 (이를테면 서비스 계층) 등은 Mocking한다. 따라서 MockMvc 를 일반적으로 사용한다.


현재 진행 중인 프로젝트에서는 이미 API를 구현하고 MockMvc 로 테스트 까지 완료한 상태이다. 그래서 Spring REST Docs 테스트도 MockMvc 를 사용할 것이다.


설정 및 실행


https://spring.io/guides/gs/testing-restdocs/

Spring의 Getting Started 가이드 중에 REST Docs 테스트를 하는 방법이 잘 나와있다. 참고하면 좋을 것 같다.

build.gradle 설정

plugins {
	
	id 'org.asciidoctor.jvm.convert' version "3.3.2"
}

configurations {
	asciidoctorExtensions
}

ext {
	set('snippetsDir', file("build/snippets"))
}

asciidoctor {
	configurations 'asciidoctorExtensions'
	sourceDir 'src/doc/asciidoc'
	attributes \
      'snippets': file('build/snippets')
}

dependencies {
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
}

tasks.named('asciidoctor') {
	inputs.dir snippetsDir
	dependsOn test
}

기존 build.gradle 에서 위의 부분들을 추가했다.

테스트 코드 작성

기존에 작성한 테스트는 @SpringBootTest 와 MockMvc를 함께 사용하고 있었다.

@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserApiControllerTest {

    @Autowired
    MockMvc mockMvc;

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    @Order(1)
    @DisplayName("회원가입 API 테스트")
    void signup() throws Exception {
        // Given
        UserDto.Signup signupForm = new UserDto.Signup(
                "tester",
                "test@gmail.com",
                "testpass!",
                "testpass!"
        );

        String content = objectMapper.writeValueAsString(signupForm);

		// When & Then
        // 요청 성공
        mockMvc.perform(post("/api/users")
                        .content(content)
                        .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andDo(document("signup"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("id").value("1"))
                .andExpect(jsonPath("username").value("tester"))
                .andExpect(jsonPath("email").value("test@gmail.com"));
                
 		...

REST Docs를 위해 @SpringBootTest 대신 @WebMvcTest 를,
@AutoConfigureMockMvc대신 @AutoConfigureRestDocs 를 적용시켰다.

@AutoConfigureRestDocs(outputDir = "build/snippets/users")
@WebMvcTest(UserApiController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserApiControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    UserService userService;

    ObjectMapper objectMapper = new ObjectMapper();

    @Test
    @Order(1)
    @DisplayName("회원가입 API 테스트")
    void signup() throws Exception {
        // Given
        UserDto.Signup signupForm = new UserDto.Signup(
                "tester",
                "test@gmail.com",
                "testpass!",
                "testpass!"
        );

        String content = objectMapper.writeValueAsString(signupForm);

        given(userService.join(any()))
                .willReturn(new UserVO(1L, "tester", "test@gmail.com"));

        // When & Then
        // 요청 성공
        mockMvc.perform(post("/api/users")
                        .content(content)
                        .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andDo(document("signup"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("id").value("1"))
                .andExpect(jsonPath("username").value("tester"))
                .andExpect(jsonPath("email").value("test@gmail.com"));

그 외에도 @WebMvcTest 는 service를 빈으로 주입하지 않기 때문에, @MockBean 을 사용하여 UserService 를 빈으로 주입하였다.

그리고 테스트 method를 자세이 살펴보면

given(userService.join(any()))
                .willReturn(new UserVO(1L, "tester", "test@gmail.com"));

가 추가된 것을 볼 수 있다. 여기서 given() 은 "해당 Mock Bean이 어떤 행동을 취하면 어떤 결과를 반환한다" 를 선언하는 부분이다.

  • 예를 들어, 위의 코드에선 @MockBean 으로 등록된 userService 가 테스트 중 join() 을 실행하면, new UserVO(1L, "tester", "test@gmail.com") 를 반환하도록 만든다.

결과

이제 테스트를 실행해서 성공하면

위와 같이 build/snippets/users/signup.adoc 이라는 확장자로 여러 문서들이 생성된 것을 확인할 수 있다.

이 중 http-request.adoc 을 살펴보면

[source,http,options="nowrap"]
----
POST /api/users HTTP/1.1
Content-Type: application/json;charset=UTF-8
Content-Length: 99
Host: localhost:8080

{"username":"tester","email":"test@gmail.com","password":"testpass!","confirmPassword":"testpass!"}
----

위와 같이 익숙한 모습의 http 요청이 문서화 된 것을 확인할 수 있다.


문서 생성

위에서 생성된 .adoc snippet을 모아 하나의 문서로 만들 수 있다.

src/doc/asciidoc directory를 생성하고 .adoc 문서 하나를 생성 하여 아래와 같이 작성하였다.

= User API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== 회원가입

.Request
include::{snippets}/users/signup/http-request.adoc[]

.Response
include::{snippets}/users/signup/http-response.adoc[]

html 생성

위의 문서를 작성한 후, 터미널에서 ./gradlew asciidoctor 을 실행하여 build를 하면 build/docs/asciidoc 에 위에서 생성한 파일과 같은 이름의 .html 이 생긴 것을 확인할 수 있다. 열어보면 아래와 같은 화면이 나온다.


어플리케이션에서 문서 확인

어플리케이션 빌드 시, 위에 생성된 html을 포함하여 같이 실행 시킬 수 있다. 그러기 위해선 build.gradle 에 다음과 같이 설정을 추가해야 한다.

bootJar {
	dependsOn asciidoctor
	from("${asciidoctor.outputDir}") {
		into 'static/docs'
	}
}

설정 추가 후, ./gradlew asciidoctor 를 한 번 더 실행 한 후,

// java -jar ./build/libs/{파일명}.jar

java -jar ./build/libs/aimage-0.0.1-SNAPSHOT.jar

위의 명령어를 실행하고 http://localhost:8080/docs/{html 파일 이름}.html 으로 접속하면 위에서 봤던 API 문서 html이 나오는 것을 확인할 수 있다.


후기

일단 Spring REST Docs는 gradle 설정, 테스트 작성, 문서 생성의 전 과정이 Swagger 보다 훨씬 복잡했다.

하지만 REST Docs의 장점은 프로덕션 코드를 건드릴 필요 없이, 테스트를 작성하는 것만으로 문서화를 할 수 있다는 것이다. 테스트 코드를 작성하고 통과할 경우에만 문서로 작성되기 때문에 Swagger와는 다르게 API 스펙과 항상 일치하는 문서를 작성할 수 있다.



Reference:

https://spring.io/guides/gs/testing-restdocs/
https://hudi.blog/spring-rest-docs/

profile
깨부하는 개발자

0개의 댓글