
본 포스팅은 Kotlin+Spring 환경에서 Spring REST Docs를 편리하게 활용하기 위해 직접 개발한 오픈소스와 관련된 내용을 담고 있습니다. 오픈소스의 소스코드는 다음 링크에서 확인하실 수 있습니다.
지난 글의 서론으로 대체합니다.
Spring Framework를 활용할 때 API 명세에 대하여 Swagger를 활용하는 것이 좋을지, Spring REST Docs를 활용하는 것이 좋을지 고민하던 때가 있었다. 필자는 현재 회사에서는 팀의 의견에 따라 Swagger를 활용하고 있는데, 개인적으로는 Spring REST Docs 쪽에 손을 들고 있다.
Swagger 편에 서서 이야기 하면 비교적 이쁜(?) 디자인과 curl call 테스트 지원을 장점으로 뽑을 수 있을 것 같다. 하지만 Swagger 어노테이션이 비즈니스 코드에 침투한다는 치명적인 단점을 갖고 있다.
반면 Spring REST Docs는 비즈니스 코드에 아무런 영향을 주지 않고, 테스트를 강제하면서 테스트 실패를 통해 API 인터페이스 변경을 감지해준다. 필자가 Restdocs를 선호하는 이유도 위와 같은데, 만약 Swagger가 제공하는 디자인과 기능이 마음에 든다면 Spring REST Docs로 명세를 하고, Swagger UI로 변환시켜줄 수 있다면 두 마리 토끼를 모두 잡을 수 있다.
하지만 Spring REST Docs를 싫어하는 사람도 적지 않은데, 가장 큰 이유가 바로 중복된 코드와 그에 따른 가독성 저하다. 아래는 Spring REST Docs를 활용하여 작성한 API 명세 예시 코드다.

더 많은 정보를 가지고 있는 Field가 제일 복잡해보이는데, header와 parameter도 optional 등의 정보를 추가한다고 가정하면 코드는 더 중복되고, 더 읽기 힘들어질 것이다.
이러한 문제들을 해결하고 싶었다. 다음은 필자가 생각했던 방향성이다.

andDocument() 확장함수를 통해 API 명세 후 다른 andDo() 작업을 할 수도 있고, 명세 목적이라면 documentation() 선언형 함수를 통해 가독성 좋게 명세가 가능하다.
어떤 기능을 개발할지 정한 후 가장 먼저 한 작업은 이미 오픈소스가 존재하는지 점검하는 것이었다. 더 있을지는 모르겠지만(찾기 꽤나 힘들다..) 다음의 두 사례를 찾을 수 있었다.
앞선 포스팅에서 Spring REST Docs에 Issue-192, Issue-547, Issue-677가 올라와있는데, spring-rest-docs-kotlin는 관련 이슈들에서 많은 언급을 받았다. 하지만 아쉽게도 오픈소스는 아니었다.
토스 페이먼츠에서는 필자가 구현하려는 내용을 이미 구현하여 사용하고 있었다. (역시 내가 생각한건 다 있구나..) 하지만 찾아보니 내부에서 사용하는 오픈소스인듯 싶다. 따라서 해당 포스팅 또한 참고하여 직접 개발하고 배포해기로 결정했다.
그렇게 개발 및 배포돼 붙여진 이름이 Korest Docs 다.
Korest Docs는 Spring REST Docs를 보다 쉽고 편리하게 사용할 수 있도록 돕는 Kotlin DSL 기반의 API 문서화 라이브러리다. Spring REST Docs는 강력한 기능을 제공하지만, 설정이 복잡하고 많은 반복적인 코드가 필요하다. Korest Docs는 이러한 문제를 해결하기 위해 Kotlin의 DSL 스타일을 적극 활용하여 더욱 직관적이고 선언적인 방식으로 API 문서를 작성할 수 있도록 개선되었다.
이제 개발자는 불필요한 보일러플레이트 코드를 줄이고, 문서화에 집중할 수 있다. Korest Docs는 타입 추론, 기본 Snippet 자동 구성, Swagger UI 변환 등 다양한 편의 기능을 제공하여, Spring REST Docs의 기능을 그대로 유지하면서도 훨씬 간결하고 생산적인 문서화 환경을 제공한다. (몇몇 기능들은 아직 개발 진행중이다)
Korest Docs는 Kotlin DSL을 활용하여 선언형 방식으로 API 문서를 작성할 수 있도록 합니다. 이를 통해 가독성이 뛰어나며, 간결하고 구조화된 API 명세를 손쉽게 작성할 수 있습니다.
fun MockMvc.requestWithDocs(
method: HttpMethod,
urlTemplate: String,
vararg vars: Any?,
dsl: MockHttpServletRequestDsl.() -> Unit = {},
): ResultActionsDsl {
return request(method, urlTemplate, *vars) {
requestAttr(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE, urlTemplate)
dsl()
}
}
기본적으로는 확장 함수를 통해 request에 Spring REST Docs를 설정해준다. 이를 통해 다음과 같이 명세가 가능하다.
mockMvc.requestWithDocs(HttpMethod.GET, "/example/{id}", 1) {
header(HttpHeaders.AUTHORIZATION, "Bearer access-token")
param("param1", "value1")
}
.andExpect {
status { isOk() }
}
.andDocument("identifier") {
requestHeader {
header("Authorization", "Access Token", "Bearer access-token")
}
pathParameter {
pathVariable("id", "id", 1)
}
requestParameter {
queryParameter("param1", "parameter 1", "value1")
}
responseField {
field("code", "Response Code", ReturnCode.SUCCESS.code)
field("message", "Response Message", ReturnCode.SUCCESS.message)
field("data", "Response Data", data)
optionalField("data.message", "message", data.message)
field("data.userId", "User Id", data.userId)
}
}
.andDo {
// 생략
}
만약 andDo()를 통한 추가 작업이 필요없다면 다음과 같이 명세할 수 있습니다.
documentation("identifier") {
request(HttpMethod.GET, "/example/{id}") {
pathVariable("id", "id", 1)
}
requestHeader {
header("Authorization", "Access Token", "Bearer access-token")
}
requestParameter {
queryParameter("param1", "parameter 1", "value1")
}
responseField {
field("code", "Response Code", ReturnCode.SUCCESS.code)
field("message", "Response Message", ReturnCode.SUCCESS.message)
field("data", "Response Data", data)
optionalField("data.message", "message", data.message)
field("data.userId", "User Id", data.userId)
}
}
이를 통해 명세에만 집중할 수 있게 되고, 가독성 또한 크게 향상된다.
Korest Docs는 요청 및 응답 모델에서 자동으로 타입을 추론하여 불필요한 반복 작업을 줄여줍니다. 다음은 FieldsSpec의 field() inline 함수다.
inline fun <reified T : Any> field(
path: String,
description: String?,
example: T,
attributes: Map<String, Any> = mapOf("optional" to false, "ignored" to false),
) {
this.field(path, description, example, T::class, attributes)
}
Kotlin에서 inline 함수를 대상으로 제공하는 Reified Type Parameter를 통해 Type 정보를 가져올 수 있다. 다음은 inline 함수에서 호출하는 field 메서드이다.
override fun <T : Any> field(
path: String,
description: String?,
example: T,
type: KClass<T>,
attributes: Map<String, Any>,
) {
fields.putIfAbsent(path, example)
val descriptor = PayloadDocumentation.fieldWithPath(path)
.type(type.toFieldType().toString())
.description(description)
.attributes(*attributes.putFormat(type).toAttributes())
add(descriptor)
}
Korest Docs는 기본 스니펫을 재구성하여 Type, Optional 등 필드가 확장된 API 문서 작성이 가능합니다. 다음은 각 요소별 필드 정보다.
| 구분 | 이름 | 타입 | 필수 | 포맷 | 설명 |
|---|---|---|---|---|---|
| path parameter | O | O | X | X | O |
| query parameter | O | O | O | O | O |
| request header | O | O | O | X | O |
| request field | O | O | O | O | O |
| request part | O | X | O | X | O |
| request part field | O | O | O | O | O |
| response field | O | O | O | O | O |
Korest Docs는 API 문서를 Swagger UI로 자동 변환하여 가시성을 높이는 기능을 제공할 계획이다. 또한, 다양한 확장 기능을 통해 API 문서 작성의 효율성을 극대화할 수 있다. 이 밖에도 여러 편의 기능들을 제공하기 위해 개발 중이다.
먼저 위의 링크를 통해 Maven Central 사이트에 들어가서 회원가입을 진행한 후 Namespace를 추가해주면 된다.
Namespace의 경우, 따로 도메인을 가지고 있지 않다면 io.github.{깃허브 계정}으로 등록해주면 된다.
우측 상단에 View Account 메뉴에 들어가 유저 토큰을 발급해야한다. 발급 후에 username과 password는 꼭 따로 기록해두어야 한다.
오픈소스가 신뢰할 수 있는 프로젝트임을 보장하기 위해서는 서명을 해야 하는데, 이 때 OpenPGP 표준을 구현한 GnuPG를 통해 무료로 암호화 할 수 있다.
# gnupg 설치
$ brew install gnupg
# 키 생성
$ gpg --full-gen-key
# 공개키를 keyserver에 등록
$ gpg --keyserver keys.openpgp.org --send-keys GPG공개키
# 비밀키 내보내기
$ gpg --keyring secring.gpg --export-secret-keys > ~/.gnupg/secring.gpg
gradle 스크립트를 작성하기 전에 지금까지 설정한 키들을 gradle.properties에 작성해줘야 한다.
mavenCentralUsername=CENTRAL_USERNAME
mavenCentralPassword=CENTRAL_PASSWORD
signing.keyId=GPG_PUBLIC_KEY
signing.password=GPG_PRIVATE_KEY_PASSWORD
signing.secretKeyRingFile=SECRET_KEY_PATH
이제 gradle 스크립트를 작성해주자. 여기서 오픈소스인 gradle-maven-publish-plugin을 활용해주었다. 아래 스크립트는 korest-docs-core의 build.gradle.kts다.
import com.vanniktech.maven.publish.SonatypeHost
. . .
mavenPublishing {
coordinates(
groupId = "${property("group")}",
artifactId = "${property("artifact")}-core",
version = "${property("version")}"
)
pom {
name.set("korest-docs")
description.set("Spring Restdocs extension library using Kotlin Dsl")
inceptionYear.set("2025")
url.set("https://github.com/lcomment/korest-docs")
licenses {
license {
name.set("The Apache License, Version 2.0")
url.set("https://www.apache.org/licenses/LICENSE-2.0.txt")
}
}
developers {
developer {
id.set("lcomment")
name.set("Hyunseok Ko")
email.set("komment.dev@gmail.com")
}
}
scm {
connection.set("scm:git:git://github.com/lcomment/korest-docs.git")
developerConnection.set("scm:git:ssh://github.com/lcomment/korest-docs.git")
url.set("https://github.com/lcomment/korest-docs.git")
}
}
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
}
이제 배포를 진행해보자. 다음의 명령어를 통해 배포할 수 있고, 설정에 문제가 없다면 배포가 성공한다.
$ ./gradlew publishAllPublicationsToMavenCentralRepository
배포 후에는 Maven Central에서 확인 가능한데, publish를 하면 취소가 불가능하니 잘 확인하고 publish를 진행하자.

이후에는 이렇게 mvnrepository에서도 확인할 수 있다. (mvnrepository에서 보기까지는 시간이 조금 걸린다.)
그럼 배포한 Korest Docs를 간단하게 사용해보자. (Document를 작성하고 있지만 아직 완성하지 못했다..)
먼저 의존성을 추가해줘야 한다.
// build.gradle.kts
dependencies {
implementation("io.github.lcomment:korest-docs-starter:1.0.0")
}
이제 활용한 테스트 코드를 확인해보자. (전체코드: 링크)
@SpringBootTest
@ExtendWith(KorestDocumentationExtension::class)
internal class ApiSpec {
@MockBean
lateinit var exampleService: ExampleService
@Test
fun `korest docs test`() {
val data = ExampleResponse()
doReturn(data).`when`(exampleService).get()
documentation("identifier") {
request(HttpMethod.GET, "/example/{id}") {
pathVariable("id", "id", 1)
}
requestHeader {
header("Authorization", "Access Token", "Bearer access-token")
}
requestParameter {
queryParameter("param1", "parameter 1", "value1")
}
responseField {
field("code", "Response Code", ReturnCode.SUCCESS.code)
field("message", "Response Message", ReturnCode.SUCCESS.message)
field("data", "Response Data", data)
optionalField("data.message", "message", data.message)
field("data.userId", "User Id", data.userId)
}
}
}
}
더 자세한 설명은 Korest Docs 공식문서에서 확인할 수 있다.

Korest Docs와 관련하여 어떤 편의 기능이 더 있으면 좋을지 항상 생각하고, 이를 ISSUE에 올려두고 있다. 관심이 있는 개발자분들이라면 한번 살펴보고 같이 작업해나가는 날이 빨리 왔으면 좋겠다.