RestDocs DSL로 API 문서화를 더 간결하게
RestDocs DSL 사내 라이브러리 설계하고 구현하기
이 글은 RestDocs, RestDocs-Api-Spec 기술에 대해서 설명이 없습니다.
기존 RestDocs API Spec 문서화 작업에서 개발자들이 겪던 문제들
- 반복적인 보일러플레이트 코드로 인한 개발 생산성 저하
- 가독성이 떨어지는 문서으로 인한 API 문서 수정의 불편함
설계 목표
- 기존 개발 패턴 유지: 팀원들이 익숙한 MockMvc 기반 테스트 작성 방식을 그대로 활용
- DSL을 통한 선언적 문서화: 가독성 있으며 유지보수가 용이한 문서 작성
- RestDocs API Spec 호환: 기존 라이브러리와 원활한 통합
- 사내 표준화: 공통 모듈로 분리하여 프로젝트 간 재사용 가능
확장 함수 기반 설계
복잡한 문법을 새로 배운다기보다는 간결하게 작성하면서 팀원들이 익숙한 MockMvc의 확장 함수로 설계했습니다.
// 기존 방식 (복잡한 설정)
MockMvcRestDocumentationWrapper.document(
"identifier",
ResourceSnippetParametersBuilder()
.description("...")
.requestFields(fieldWithPath("name").description("이름"))
// ... 복잡한 설정들
)
// 개선된 DSL 방식 (간결하고 직관적)
mockMvc.document(get("/api/users")) {
identifier = "UserController/getUsers"
summary = "사용자 목록 조회"
requestFields = listOf(
"name" type STRING means "사용자 이름" sample "홍길동"
)
}
버전 안정성과 브릿지 설계
fun MockMvc.document(
requestBuilder: RequestBuilder,
block: ApiDocumentationBuilder.() -> Unit,
): ResultActions {
val builder = ApiDocumentationBuilder().apply(block)
val spec = builder.build()
return this.perform(requestBuilder)
.andDo(
apiSpecDocument(
identifier = spec.identifier,
resourceDetails = createResourceDetails(spec),
snippets = createSnippets(spec)
)
)
}
핵심 설계 원칙:
- RequestBuilder는 RestDocs의 표준 API이기 때문에, RestDocs의 버전이 올라가더라도 호환성 유지
- ApiDocumentationBuilder는 외부에 노출되지 않고 내부에서만 사용되므로, RestDocs API Spec 라이브러리의 의존성이 바뀌어도 기존 테스트 코드에는 영향 없음
- API 문서 작성 DSL과 문서 생성 구현부를 명확히 분리하여 안정적인 유지보수와 확장을 고려
선언적 문서화 패턴
복잡한 보일러플레이트 코드 없이 문서화에 집중 가능한 코드:
responseFields = successResponseFields(
"data.id" type NUMBER means "사용자 ID" sample user.id,
"data.name" type STRING means "사용자 이름" sample user.name,
"data.status" type ENUM(UserStatus::class) means "사용자 상태" sample user.status.name
)
Builder 패턴과 Spec 변환
class ApiDocumentationBuilder {
var identifier: String = ""
var summary: String? = null
var requestFields: List<Field> = emptyList()
var responseFields: List<Field> = emptyList()
// ... 기타 필드들
fun build(): ApiDocumentationSpec =
ApiDocumentationSpec(...)
}
RestDocs API Spec의 필요한 값들을 래핑한 빌더 클래스를 만들어 파라미터로 받고,
build() 함수를 호출할 때 적용하도록 구현했습니다.
ENUM 타입 자동 문서화
fun getEnumNames(docsFieldType: DocsFieldType): List<String>? {
return if (docsFieldType is ENUM<*>) {
docsFieldType.enums.map { (it as Enum<*>).name }
} else null
}
"status" type ENUM(UserStatus::class) means "사용자 상태"
// → "사용자 상태 [ACTIVE, INACTIVE, PENDING]"
- ENUM 타입은 ENUM(MyEnum::class)로 클래스를 지정하면 Array 형태로 문서에 모든 ENUM 값이 포함됨니다.
- 이 구조 덕분에 ENUM 정의가 변경되면 문서도 자동으로 최신화되며, 수동으로 ENUM 값 목록을 수정할 필요 없습니다.
템플릿 기반 공통 응답 구조
fun successResponseFields(vararg fields: Field): List<Field> {
val baseFields = listOf(
"code" type NUMBER means "HTTP 상태 코드" sample "200",
"message" type NULL means "응답 메시지" isOptional true
)
return baseFields + fields.toList()
}
사내에서 성공 응답 시 공통적으로 내려주는 기본 필드 값들을 명시하는
성공 응답 템플릿 함수를 만들어 반복되는 코드를 줄였습니다.
핵심 컴포넌트 분석
1. MockMvc.document - restdocs-api-spec를 래핑한 확장 함수
MockMvc의 확장 함수로 기존 테스트 코드와 자연스럽게 통합되며, DSL 블록을 통한 선언적 문서 정의와 RestDocs API Spec과의 완전한 호환성을 제공합니다.
2. Field.kt & DocsFieldType.kt - 타입 안전한 필드 시스템
infix 함수를 통한 자연어에 가까운 DSL 구문을 제공하며, Sealed class로 타입 안전성을 보장합니다.
3. ApiResultTemplates.kt - 표준화된 응답 구조
팀 내 공통 응답 템플릿 함수로, 프로젝트 전체에서 일관된 API 응답 구조를 제공하여 개발자의 반복 작업을 최소화하고 문서 품질의 일관성을 확보합니다.
사용 예시 (Example)
아래 코드는 실제 프로젝트 코드가 아닌, DSL 사용 방법을 설명하기 위한 Example 예시입니다:
mockMvc.document(post("/api/v1/banners")) {
identifier = "BannerController/createBanner"
tag = BANNER_TAG
summary = "배너 생성"
description = """
새로운 배너를 생성합니다.
"""
requestFields = listOf(
"title" type STRING means "배너 제목" sample request.title,
"status" type ENUM(BannerStatus::class) means "배너 상태" sample request.status.name,
"description" type STRING means "배너 설명" isOptional true
)
responseFields = successResponseFields(
"data.id" type NUMBER means "생성된 배너 ID" sample response.id,
"data.title" type STRING means "배너 제목" sample response.title
)
}
정리
안정성과 호환성을 유지하면서 MockMvc 기반의 확장 함수와 DSL을 설계하여 문서화를 개선했습니다. 이를 통해 API 문서 작성 시 코드량을 60%(50줄 → 20줄) 감소시키고, 자연어에 가까운 선언식 문법으로 가독성을 대폭 향상했습니다. 또한 ENUM 타입을 클래스 단위로 자동 최신화되도록 구현하여, 문서 수정이라는 부수적인 업무의 불편함을 해소했습니다.
Comments