본문으로 바로가기

프로젝트의 전체 소스 코드는 이곳에서 확인하실 수 있습니다.

프로젝트에 Spring rest docs 적용하기

현재 진행하고 있는 Shoe-auction 프로젝트에서 API 문서를 작성하는 과정에서 사용한 Spring rest docs의 적용 과정을 소개하려고 한다.

Spring API 문서 자동화에는 대표적으로 SwaggerSpring Rest Docs가 사용된다.

두 개의 장단점을 소개한 여러 글들이 있지만 이번 프로젝트에서 Spring Rest Docs을 선택한 이유는 단 한 가지다. Swaager를 사용하게 되면 비즈니스 로직과 관련없는 어노테이션을 추가해야하기 때문에 프로덕션 코드가 오염된다고 생각했기 때문이다.

 

<Swaager를 적용한 컨트롤러 코드 예시>

@RestController
@Api(value = "SwaggerTestController")
@RequestMapping("/v1/test")
public class SwaggerTestController {
    @ApiOperation(value = "test", notes = "테스트입니다.")
    @ApiResponses({
        @ApiResponse(code = 200, message = "OK~!"),
        @ApiResponse(code = 404, message = "page not found!!!")
    })
    @GetMapping(value = "/board")
    public Map<String, String> selectBoard(@ApiParam(value = "게시판번호", required = true, example = "1") @RequestParam String no) {
        Map<String, String> result = new HashMap<>();
        result.put("test title", "테스트");
        result.put("test content", "테스트 내용");
        return result;
    }
}

위 코드처럼 이전에도 복잡한 컨트롤러 코드에 Swaager 관련 어노테이션이 추가되어 더 복잡해진 것을 확인할 수 있다.

사실 Spring rest docs를 적용하는 방법은 우아한 형제들 기술 블로그Spring Rest Dcos 공식문서 에 아주 잘 나와있다.

이번 포스팅에서는 Spring rest docs을 적용하면서 겪었던 삽질(?)들과 함께 사용 방법을 공유해보려고 한다.

1. build.gradle 설정하기

// 의존성 추가  
dependencies {
    asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' 
    testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' 
}


// Asciidoctor 플러그인을 적용하여  AsciiDoc 파일을 컨버팅합니다.
plugins { 
    id "org.asciidoctor.convert" version "1.5.9.2"
}

// build/generated-snippets 경로에 스니펫이 생성됩니다.
ext { 
    snippetsDir = file('build/generated-snippets')
}


// gradle build시 test -> asciidoctor 순으로 실행됩니다.
asciidoctor { 
    inputs.dir snippetsDir 
    dependsOn test 
}


// gradle build시 ./build/asciidoc/html/ 에 html 파일이 생성됩니다.
bootJar {
    dependsOn asciidoctor
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}


test { 
    outputs.dir snippetsDir
}

2. 테스트 코드 작성하기

Spring rest docs 사용하기 위해서는 테스트 코드를 필수로 작성해야 한다.

@WebMvcTest는 web 레이어 관련 빈(Controller 관련 빈)들만 등록해서 사용하므로 @SpringBootTest에 비해 비교적 가볍고 빠른 속도로 테스트가 가능하다. 따라서 @WebMvcTest을 이용한 테스트코드를 작성하기로 했다.

먼저 테스트코드에 사용되는 어노테이션을 살펴보자.

@ExtendWith(RestDocumentationExtension.class) // (1)
@WebMvcTest(UserApiController.class)  // (2)
@ActiveProfiles("test") // (3)
@MockBean(JpaMetamodelMappingContext.class) // (4)
class UserApiControllerTest {
    //...생략
}
  1. JUnit 5 를 사용할 때 문서 스니펫 생성을 위해 RestDocumentationExtension 을 테스트 클래스에 적용해야 한다.
  2. Controller 관련 빈들만 등록되는 @WebMvcTest를 사용한다.
  3. 설정파일(.yml / .properties)이 나누어져 있다면, 테스트에서 사용할 profile을 설정한다.
  4. JPA Auditing 기능 사용을 위해 application에 @EnableJPaAuditing 을 적용했다면, 해당 어노테이션을 추가해야 한다. @WebMvcTest는 JPA 생성과 관련된 기능을 제공하지 않는다.

다음으로 Spring Rest docs을 사용하기 위한 MockMvc 설정을 추가해야 한다.

@ExtendWith(RestDocumentationExtension.class) 
@WebMvcTest(UserApiController.class)  
@ActiveProfiles("test") 
@MockBean(JpaMetamodelMappingContext.class) 
class UserApiControllerTest {

    private MockMvc mockMvc;

    //Spring Rest Docs 사용을 위한 MockMvc 설정
    @BeforeEach
    public void setup(WebApplicationContext webApplicationContext,
        RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
            .apply(documentationConfiguration(restDocumentation))
            .apply(sharedHttpSession())
            .build();
    }
}

Spring Rest Docs을 사용하기 위해 위 코드처럼 MockMvc 설정을 직접 추가하는 방법 외에, 아래 코드처럼 간단하게 어노테이션 하나로 설정을 끝내는 방법도 존재한다.

@ExtendWith(RestDocumentationExtension.class) 
@WebMvcTest(UserApiController.class)  
@ActiveProfiles("test") 
@MockBean(JpaMetamodelMappingContext.class) 
@AutoConfigureRestDocs 
class UserApiControllerTest {

    @Autowired
    private MockMvc mockMvc;

@AutoConfigureRestDocs 만 추가해주면 된다. 둘 중 편한 방법을 선택하자.

테스트 코드 작성 - rest docs 적용 전
// 어노테이션 생략
class UserApiControllerTest {

    // mocking하기 위한 어노테이션
    @MockBean
    private UserService userService;

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @Test
    @DisplayName("회원가입 - 모든 유효성 검사에 통과했다면 회원가입에 성공한다.")
    void createUser_successful() throws Exception {
        SaveRequest saveRequest = SaveRequest.builder()
            .email("jungkh405@naver.com")
            .password("test1234")
            .nickname("17171771")
            .phone("01012345678")
            .build();

        //mocking을 하여 예상 응답 값 받기(void일 경우 doNothing() 사용)
        doNothing().when(userService).save(saveRequest);

        mockMvc.perform(post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(saveRequest)))
            .andDo(print())
            .andExpect(status().isCreated());
    }
}

해당 코드는 Spring Rest Docs를 적용하지 않은 테스트코드이다. 위 설정만 잘 따라왔다면 평소에 작성하던 테스트코드에서 조금만 추가하면 아주 쉽게 Spring Rest Docs를 완성할 수 있다.

테스트코드 - rest docs 적용

//...생략 
        mockMvc.perform(post("/users")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(saveRequest)))
            .andDo(print())
            .andExpect(status().isCreated())

            .andDo(document("users/create/successful", requestFields(
                fieldWithPath("email").type(JsonFieldType.STRING)
                    .description("The user's email address"),
                fieldWithPath("password").type(JsonFieldType.STRING)
                    .description("The user's password"),
                fieldWithPath("nickname").type(JsonFieldType.STRING)
                    .description("The user's nickname"),
                fieldWithPath("phone").type(JsonFieldType.STRING).description("The user's phone")
            )));
    }

requestFields, pathParameters , responseFields 등 다양한 메서드들을 제공한다. 메서드에대한 자세한 정보는 Spring Rest Dcos 공식문서를 참고하자.

테스트코드에 Spring Rest Docs 관련 코드까지 추가하고 테스트를 실행 후 테스트가 정상적으로 통과되었다면 아래 그림과 같이 해당 경로에 snippets이 생성된다.

기본경로 : ./build/generated-snippets

.andDo(document("users/create/successful")) // snippets이 생성되는 경로

snipperts이 생성되는 경로

3. 생성된 snippets으로 API 문서 작성하기

snippets 작성은 아래 경로에 .adoc 확장자로 asciidoc 파일을 만들어 작성해야 한다.

gradle vs maven 소스파일 경로

asciidoc 파일을 생성할때 Gradle과 Maven의 생성 경로가 다르다는 점을 유의해야 한다. 만약 Maven을 사용하는데 src/docs/asciidcs 경로에 .adoc을 생성한다면 html 파일이 생성되지 않는다.

필자는 Gradle을 사용하므로 아래와 같은 경로에 asciidoc 파일을 생성했다.

api-guide.adoc 작성 예시

=== 1. CREATE USER

==== 회원가입

`*_1. Success_*`

**request-fields**

include::{snippets}/users/create/successful/request-fields.adoc[]

**Example request**
include::{snippets}/users/create/successful/http-request.adoc[]

**Example response**

include::{snippets}/users/create/successful/http-response.adoc[]


---

asciidoc에 API 문서를 작성 후 gradle build를 진행하면 ./build 폴더에 asciidoc 이라는 폴더가 생성되면서 그 안에 html 파일이 생성된다.

추가적으로 intelliJ 사용자 라면 AsciiDoc 플러그인을 다운로드 받아 .adoc 파일에서 편하게 작업이 가능하다.

Tasks - build - build

Tasks - build - build를 실행하면 gradle에서 설정한 순서에 따라 build가 진행된다. build가 완료되면 아래 경로에 html 파일이 생성된걸 확인할 수 있다.

html 생성 경로

완성된 html 파일

완성된 API 문서

주의할 점

마지막으로 프로젝트에 Spring rest docs를 적용하면서 겪은 삽질을 공유하고자 한다.

해당 프로젝트는 profile을 다음과 같이 나누어서 사용하고 있다.

Tasks - build - build를 실행하면 test - > asciidoctor -> bootJar 순으로 빌드가 진행된다.

프로젝트를 개발하는 단계에서는 application-local.yml을 기본 Active profile로 사용하고 있다.

하지만 gradle build 진행시 application.yml 파일을 Active profile로 빌드를 진행하기 때문에 외부 주입으로 사용된 값들에 의해 NPE가 발생할 수 있다. 따라서 bulid.gradle 에서 build시 사용할 profile을 설정해야 한다.