min.c00 2024. 6. 10. 22:34

이번 포스팅에서는 책에서 소개한 내용과는 조금 다른 내용으로 포스팅하려고 합니다. 책에서는 API 문서화를 위한 오픈소스로 Spring Fox를 사용했지만 더 이상 spring fox에 대한 지원을 하지 않는다는 내용을 확인하고 다른 API 문서화를 위한 오픈소스를 찾아보기로 했습니다. 

 

https://github.com/springfox/springfox/issues/4041

 

API 문서화를 위한 다양한 오픈소스들이 있지만 제가 선택한 방법은 Spring REST Docs입니다. 해당 오픈소스를 선택한 이유에 대해서 SpringFox와 비교해서 설명하면 다음과 같습니다. 

  • Spring REST Docs 테스트 실행시 정적으로 API 문서를 생성합니다. 

SpringFox는 런타임 시에 동적으로 API를 생성하는데 API 정의가 자주 바뀌는 게 아니라면 정적으로 문서를 생성하는 것이 효율적이라고 생각했습니다. Spring REST Docs는 테스트 코드를 우선적으로 작성하고 테스트 코드가 통과하게 되면 미리 정의한 호출 스펙에 맞게 API 문서를 생성합니다. 

  • 테스트 코드로 인해 생성되는 API 문서라는 점에서의 매력

Spring REST Docs는 앞서 언급한것 처럼 테스트 코드가 통과됐을 때 생성이 되는 만큼 정확하고 안전하다는 게 매력적이었습니다. controller와 Request Payload에 대한 호출 스펙에 대한 제한을 엄격하게 준수해야만 테스트 코드를 통과하기 때문입니다. 실제 요청과 응답을 기반으로 문서화되므로 신뢰성이 높다는 점도 SpringFox와 비교했을 때 큰 장점이라고 생각했습니다. 

  • 비즈니스 로직에서 완벽한 분리 

마지막 장점도 API 문서의 생성이 테스트 코드와 연계하여 생성된다는 점에서 파생한 장점인데, 테스트 코드로써 API 정의를 하다보니 비즈니스 로직에서 완벽히 분리된다는 게 매력적이었습니다. SpringFox의 경우 어노테이션 기반으로 API 정의서를 적성해야 하고, Docket과 같은 스웨거를 위한 설정을 위한 Config 용 코드가 필연적이라는 점에서 조금 불편한 감이 없잖아 있었습니다. 반면에 Spring REST Docs는 테스트 코드와 연계해서 API 문서를 작성하기에 별도의 Config 가 필요 없고 Controller의 가독성을 높여준다는 점에서 장점으로 느껴졌습니다. 


Spring REST Docs 사용법

1. 의존성(dependencies)

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

 

 

  • spring-restdocs-mockmvc는 테스트를 통해 API 요청과 응답을 캡처하고, 이를 문서화하는 스니펫을 생성하는 역할을 담당합니다. 스니펫(snippet)은 코드나 문서의 일부를 나타내는 짧은 텍스트 조각이라는 뜻으로 Spring REST Docs에서 스니펫은 API 엔드포인트에 대한 요청과 응답의 세부 정보를 포함하는 작은 문서 조각을 의미합니다. (자새한 내용은 아래에서 추가로 설명하겠습니다.)
  • spring-restdocs-asciidoctor는 이러한 스니펫을 사용하여 최종 API 문서를 생성하는 데 필요합니다.

2. 스니펫(snippet) 디렉터리 설정

ext {
    snippetsDir = file("build/generated-snippets")
}

 

해당 설정은 테스트 실행 시 생성되는 REST Docs 스니펫 파일들이 저장될 디렉터리를 설정합니다. 

 

이 내용을 이해하기 위해서는 스니펫의 생성과정, 스니펫의 종류, 저장위치에 대해 먼저 이해해야 합니다. 

  • 스니펫 생성 과정

Spring REST Docs 은 테스트코드를 실행할 때 API 요청과 응답의 세부 정보를 캡처합니다. 여기서 말하는 캡처란, API 요청 . 및 응답의 저장을 의미합니다. 이해를 돕기 위해 코드를 보면서 설명하겠습니다. 

@Test
public void documentApi() throws Exception {
    this.mockMvc.perform(get("/api/test"))
            .andExpect(status().isOk())
            .andDo(document("api-test",
                    preprocessRequest(prettyPrint()),
                    preprocessResponse(prettyPrint())));
}

 

 

다음과 같은 테스트 코드가 있다고 가정해보겠습니다. 해당 코드는 /api/test라는 엔드포인트로 get요청을 보냈을 때 응답값이 200인지 확인하는 테스트 코드입니다. andExpect() 함수가 true를 반환하면 andDo함수가 콜백으로 실행되는데, 해당 코드의 의미는 Spring REST Docs에게 요청 및 응답 정보를 캠 처하고 "api-test"라는 이름의 스니펫을 생성하도록 지시함을 의미합니다. 

 

이 지시로 인해 생성되는 스니펫은 

// http-request.adoc

[source,http]
----
GET /api/test HTTP/1.1
Host: localhost:8080
----

// http-response.adoc

[source,http]
----
HTTP/1.1 200 OK
Content-Type: application/json
[
  {
    "id": 1,
    "name": "Test"
  }
]
----

 

http-request.adoc, http-response.adoc 등의 스니펫들이 생성됩니다.

  • 스니펫의 종류

스니펫의 종류는 HTTP 요청, 응답외에도 문서화를 위한 스니펫들, 예를 들어 요청 필드/ 링크/ 매개변수 등을 설명하는 스니펫도 생성을 테스트코드에서 지시할 수 있습니다. 

  • 스니펫 저장위치 

스니펫은 일반적으로 build/generated-snippets 디렉토리에 저장됩니다. 위에서 언급한 스니펫(snippet) 디렉터리 설정과 같습니다. 

 

3. 테스트 설정

test {
    outputs.dir snippetsDir
    useJUnitPlatform()
}

 

  • outputs.dir snippetsDir: 테스트 출력 디렉토리를 snippetsDir로 설정합니다. 이는 REST Docs 스니펫 파일들이 이 디렉터리에 저장되도록 합니다. 위에서 지정한 build/generated-snippets 가 저장 위치가 되도록 설정했습니다. 
  • useJUnitPlatform(): JUnit Platform을 사용하도록 설정합니다. 이는 JUnit 5를 사용하여 테스트를 실행하는 데 필요합니다.

4. Asciidoctor 설정

asciidoctor {
    dependsOn test
    configurations 'asciidoctorExtensions'

    sources {
        include("**/index.adoc")
    }
    baseDirFollowsSourceFile()
}

 

 

  • dependsOn test: Asciidoctor 작업이 실행되기 전에 test 작업이 완료되도록 설정합니다. 즉, 테스트를 먼저 실행하여 스니펫을 생성한 후에 Asciidoctor를 실행하도록 하는 설정 정보입니다. 
  • configurations 'asciidoctorExtensions': Asciidoctor 확장을 위한 설정입니다.
  • sources { include("/index.adoc") }**: Asciidoctor가 변환할 소스 파일을 지정합니다. 여기서는 index.adoc 파일을 포함시킵니다. 
  • baseDirFollowsSourceFile(): Asciidoctor의 기본 디렉토리를 소스 파일의 위치에 따르게 합니다. Asciidoctor는 Asciidoc 파일을 HTML 또는 다른 형식으로 변환할 때, 이미지, 스타일시트 등 추가 리소스를 참조할 수 있는데 이러한 리소스를 찾기 위한 기본 디렉터리 설정을 의미합니다. 

5. Asciidoctor 작업 전후 처리 

asciidoctor.doFirst {
    delete file('src/main/resources/static/docs')
}

 

 

이 설정 정보는 Asciidoctor 작업이 실행되기 전에 src/main/resources/static/docs 디렉토리를 삭제함을 명시합니다. 이유는 이전에 생성된 문서 파일들을 삭제함으로써 중복을 방지하는 역할을 합니다. 

 

6. 문서 복사 작업

task copyDocument(type: Copy) {
    dependsOn asciidoctor
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

 

해당 작업은 asciidoctor 작업이 완료된 후에 실행되며, asciidoctor 작업에서 생성된 html 파일들을 복사해서 src/main/resources/static/docs 디렉터리에 저장하는 설정정보를 의미합니다. 복사한 파일들을 정적 리소스로 사용할 수 있도록 하는 역할을 담당합니다. 

 

7. 빌드 설정

build {
    dependsOn copyDocument
}

 

해당 설정정보는 copyDocument 작업이 실행되도록 설정합니다. 즉, 빌드 과정에서 문서 파일이 자동으로 복사되게 합니다. 

 

8. BootJar 설정

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

 

이 작업은 bootJar 작업이 실행되기 전에 asciidoctor 작업이 완료되도록 설정합니. asciidoctor 작업에서 생성된 파일들을 static/docs 디렉터리에 포함시켜 최종 jar 파일에 문서 파일을 포함시킵니다. 

 

9. 테스트 코드 작성

package backend.api;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@ExtendWith({RestDocumentationExtension.class, SpringExtension.class})
@SpringBootTest
@AutoConfigureMockMvc
public class ApiApplicationTests {

	@Autowired
	private WebApplicationContext context;

	private MockMvc mockMvc;

	@BeforeEach
	public void setUp(RestDocumentationContextProvider restDocumentation) {
		this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
				.apply(documentationConfiguration(restDocumentation))
				.build();
	}

	@Test
	public void documentGetEndpoint() throws Exception {
		this.mockMvc.perform(get("/api/test"))
				.andExpect(status().isOk())
				.andDo(document("restDocs-test"));
	}
}

 

이제 가장 중요한 테스트 코드 부분을 보겠습니다. 아래 documentGetEndpoint() 함수는 앞서 언급한것 처럼 스니펫을 생성하는 부분입니다. setup함수는 각 테스트 메서드가 실행되기 전에 Spring REST Docs를 사용해 MockMvc를 설정하고, API 요청 및 응답을 문서화하는데 필요한 환경을 구성하는 역할을 합니다. 

 

10. adoc 파일 작성

// index.adoc

= API Documentation

include::Test-API.adoc[]

// Test-API.adoc

== Test API

operation::restDocs-test[snippets='http-request,http-response']

 

adoc 파일은 스니펫을 활용해서 실제 API 문서를 만들기 위한 파일입니다. index.adoc 파일에서 문서에 포함시킬 adoc 파일을 include 하고 included 된 파일 내에서는 사용할 스니펫을 명시합니다. 이전에 테스트 코드에서 restDocs-test라는 이름으로 스니펫이 생성되었기 때문에 해당 스니펫을 사용합니다. 

 

11. build

마지막으로 gradle build 를 통해 테스트 코드를 포함한 빌드를 진행하면 테스트 성공과 함께 API 문서를 성공적으로 만들 수 있습니다. 

생성된 API 문서는 다음과 같습니다. 

반응형