본문 바로가기
🌱BackEnd/Spring

Spring REST Docs 사용해보기

by Dong Ik 2023. 9. 7.

백엔드 개발자끼리만 개발할 때는 API를 짤 일이 없었지만 프론트 혹은 어플리케이션 개발자와 협업하기 위해서는 API 문서가 필수적입니다. 문서를 통해 개발 된, 개발 할 API 문서를 공유할 수 있으며 입력과 출력의 형식, 제약사항 등을 명시할 수 있으므로 신뢰성있고 통일성 있는 작업이 가능하게 됩니다.

이번에 iOS 개발자와 협업하게 되면서 필수적으로 API 명세서를 작성해야 했는데, Spring REST Docs를 통해 작성하기로 했고, CICD 파이프라인를 통해 지속적인 배포까지 구현해보았습니다.

 

Swagger vs Spring REST Docs

문서화를 고려하면서 가장 많이 언급되는 두 가지의 방식 중 하나를 선택해야 했습니다.

Swagger와 Spring REST Docs 입니다.

각각의 장단점을 살펴보면

1. Swagger

  • 간단한 어노테이션만으로 문서 작성이 가능합니다.
  • API 문서를 통해 간단하게 테스트가 가능합니다.

2. Spring REST Docs

  • 통과된 테스트 기반으로 문서가 작성되므로 API 문서에 신뢰성이 보장됩니다.
  • 기본 템플릿이 Swagger보다 투박하며 가독성이 다소 떨어집니다.

하지만 Swagger는 어노테이션 기반이기 때문에 Controller 코드에 Swagger 코드가 덕지덕지 붙게 되는 점이 불편했고 Spring REST Docs는 세팅과정이 불편하고 디자인도 구리지만 테스트 기반이기 때문에 프로덕트 코드가 깔끔해집니다. 따라서 Spring REST Docs를 사용하기로 했습니다.

 

 

Spring REST Docs

우선 개발 환경은 다음과 같습니다.

  • spring boot 3.1.3
  • gradle 8.2.1

1. 동작 과정 요약

기본 동작 과정을 살펴보고 세팅을 해보겠습니다.

  1. 테스트 작성: Spring REST Docs는 TEST 기반 문서입니다. 따라서 API 엔드포인트에 대한 테스트를 작성합니다.
  2. 테스트 실행: 작성한 테스트를 실행합니다. API의 동작을 검증합니다.
  3. 스니펫 생성: Spring REST Docs가 테스트 결과를 통해 스니펫을 생성합니다. 스니펫은 API의 요청과 응답 , Path Param, Query Param 등에 관한 정보가 포합됩니다.
  4. 문서 작성: 생성된 스니펫을 사용하여 문서 템플릿을 작성합니다.
  5. 문서 생성: Spring REST Docs는 스니펫과 템플릿을 결합하여 API 문서를 생성합니다.
  6. 문서 빌드: 생성된 문서를 빌드하여 HTML이나 PDF로 변환합니다.
  7. 문서 배포: 생성된 문서를 배포하여 다른 개발자와 공유합니다.

 

2. gradle 세팅

우선 gradle에 아래 코드를 모두 추가해야 합니다.

ext { //스니펫이 생성되는 위치, 테스트 성공시 아래 경로로 스니펫이 생성됨
	set('snippetsDir', file("build/generated-snippets"))
}

configurations { //asciidoctorExt을 Configuration에 지정
	asciidoctorExt
}

asciidoctor {
	inputs.dir snippetsDir //input 디렉토리 설정, 위에 ext에 명시됨
	configurations 'asciidoctorExt' //asciidoctor에서 위에서 명시한 Ext를 사용하도록 설정
	dependsOn test //테스트 이후 asciidoctor가 동작하도록 순서 명시
}

test {
	useJUnitPlatform()
	outputs.dir snippetsDir //테스트 성공시 위에 명시한 snippetDir 디렉토리로 파일 저장
}

asciidoctor.doFirst { //asciidoctor가 실행하기 전에 직전에 생성된 API 문서 삭제
	delete file('src/main/resources/static/docs') 
}

task copyDocument(type: Copy) { //생성된 문서를 resources 폴더에 복사
	dependsOn asciidoctor
	from file("build/docs/asciidoc") 
	into file("src/main/resources/static/docs")
}

build { //빌드시 위에서 명시한 copy가 수행됨
	dependsOn copyDocument
}

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

간단하게 요약하자면 우선 스니펫과 문서 생성 위치, 결과물 복사 위치 등을 세팅해둡니다.

그리고 테스트 성공 시 지정된 디렉토리에 스니펫을 생성하며, 빌드 성공 시에는 완성된 API 문서를 resouces 폴더에 복사를 합니다.

 

3. 문서 템플릿 생성

여기다 생성

 

출력될 문서의 템플릿은 src/docs/asciidoc 디렉토리 안에 index.adoc 이라는 파일 명으로 생성합니다.

index.adoc

= API 명세서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:sectnums:
:docinfo: shared-head

이제 이 템플릿 아래에 test 후 생성된 스니펫을 순서대로 적어주면 api 문서가 완성됩니다.

 

4. 테스트 코드 작성 시 스니펫이 저장되는 위치 설정

테스트 코드 작성 후 생성되는 스니펫의 세부적인 디렉토리를 세팅하면 관리하기도 편합니다.

클래스이름/메서드이름 이라는 디렉토리에 스니펫이 저장되게 됩니다.

문서에 접근할 때 localhost:8080 보다는 https://docs.api.com 으로 별개의 URL로 접근하는게 편하겠죠?

관련 세팅도 추가해줍니다.

RestDocsConfiguration

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;
import org.springframework.restdocs.operation.preprocess.Preprocessors;

@TestConfiguration
public class RestDocsConfiguration {

  @Bean
  public RestDocumentationResultHandler write() {
    return MockMvcRestDocumentation.document(
        "{class-name}/{method-name}",
        Preprocessors.preprocessRequest(
																				 modifyUris()
											                        .scheme("https")
											                        .host("docs.api.com")
											                        .removePort(),
																					Preprocessors.prettyPrint()),
        Preprocessors.preprocessResponse(Preprocessors.prettyPrint())
    );
  }
}

 

5. 추상 클래스 선언

컨틀롤러 테스트에만 RestDoc을 적용할 예정이기 때문에 컨트롤러 테스트를 위한 추상 클래스를 선언합니다.

앞으로 작성되는 컨틀롤러 WebMvcTest는 해당 추상 클래스를 구상하게 됩니다.

@WebMvcTest({
        MemberController.class
})
@ExtendWith(RestDocumentationExtension.class)
public abstract class ControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    protected MemberService memberService;

    @BeforeEach
    void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentation))
                .build();
    }
}

 

6. 실제 테스트 작성

 class MemberControllerTest extends ControllerTest {

    SignUpRequest signUpRequest;

    @BeforeEach
    void setUp() {
        signUpRequest = SignUpRequest.builder()
                .email("test@test.com")
                .password("qwer1234")
                .passwordConfirmation("qwer1234")
                .build();
    }

    @Test
    void signUp() throws Exception {
        // given
        doNothing().when(memberService).signUp(any());

        // when
        ResultActions result = mockMvc.perform(post("/members/signup")
                .content(objectMapper.writeValueAsString(signUpRequest))
                .contentType(MediaType.APPLICATION_JSON_VALUE));

        // then
        result.andExpectAll(status().isCreated());
    }
}

 

7. API 문서에 스니펫 추가

위 테스트가 정상적으로 동작됐다면 스니펫이 생성 됩니다.

해당 스니펫의 파일 위치와 가져올 스니펫 목록을 아래와 같이 index 파일에 추가하면 API 문서가 완성이 됩니다!

index.adoc

= API 명세서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
:sectnums:
:docinfo: shared-head

== 회원
=== 회원가입
operation::member/signUp[snippets='request-fields,http-request,http-response']

 

 

후기

다음은 CICD 파이프라인을 구축해서 API 문서와 서버가 지속적으로 운영되도록 해보겠습니다!

 

 

 

참고

Spring REST Docs

Spring Rest Docs 적용 | 우아한형제들 기술블로그