Spring RestDocs vs Swagger 비교: 테스트 기반 API 문서 자동화의 정석
1. 서론
백엔드 개발자와 프론트엔드 개발자(혹은 모바일 앱 개발자) 사이에서 가장 빈번하게 발생하는 갈등의 원인은 무엇일까요? 바로 “API 문서와 실제 동작의 불일치“입니다.
“문서에는 필드명이 userId라고 되어 있는데 왜 실제로는 id가 오나요?”, “이 API는 404 에러가 없다고 되어 있는데 왜 404가 뜨나요?”와 같은 질문을 받는 순간, 백엔드 개발자는 부끄러움을 느끼며 부랴부랴 문서를 수정하러 갑니다.
문제의 핵심은 “코드 수정과 문서 수정이 분리되어 있다“는 점에 있습니다. 바쁜 일정 속에서 비즈니스 로직을 수정하고 배포하는 데 급급하다 보면, 위키(Wiki)나 노션(Notion), 혹은 스웨거(Swagger) 어노테이션을 업데이트하는 것을 깜빡하기 십상입니다. 사람이 수동으로 관리하는 문서는 시간이 지날수록 반드시 코드와 괴리감이 생기며, 결국 ‘믿을 수 없는 문서’가 되어버립니다. 이를 ‘문서의 부패(Documentation Rot)’라고 부릅니다.
이러한 문제를 해결하기 위해 등장한 것이 바로 ‘테스트 코드 기반의 문서 자동화’ 도구인 Spring RestDocs입니다. 오늘은 API 문서화의 영원한 라이벌인 Swagger(OpenAPI)와 Spring RestDocs를 아키텍처 관점에서 비교하고, “테스트가 통과하지 않으면 문서가 생성되지 않는” 가장 강력하고 신뢰할 수 있는 API 문서 자동화 시스템을 구축하는 방법을 알아보겠습니다.
2. 본론
1. Swagger(Springdoc) vs Spring RestDocs 심층 비교
두 도구 모두 훌륭한 솔루션이지만, 지향하는 철학이 완전히 다릅니다. 프로젝트의 성격에 따라 올바른 도구를 선택해야 합니다.
1. Swagger (Springdoc OpenAPI)
Swagger는 ‘편의성‘과 ‘상호작용‘에 초점을 맞춥니다.
- 장점:
- 적용이 쉽다: 의존성만 추가하고 컨트롤러에
@Tag,@Operation같은 어노테이션만 붙이면 화려한 웹 UI가 뚝딱 만들어집니다. - API 테스트 가능 (Try it out): 문서 화면에서 바로 요청을 보내고 응답을 확인할 수 있어, 포스트맨(Postman) 대용으로 훌륭합니다.
- 적용이 쉽다: 의존성만 추가하고 컨트롤러에
- 단점 (치명적):
- 프로덕션 코드 오염: 비즈니스 로직이 담긴 컨트롤러 클래스가 문서화를 위한 수많은 어노테이션으로 뒤덮이게 됩니다. 코드 가독성을 심각하게 해칩니다.
- 신뢰성 문제: 어노테이션은 주석과 같습니다. 코드 로직이 바뀌어도 어노테이션을 수정하지 않으면 문서는 그대로 유지됩니다. 즉, “거짓말하는 문서“가 될 가능성이 매우 높습니다.
2. Spring RestDocs
Spring RestDocs는 ‘신뢰성‘과 ‘코드 청정성‘에 초점을 맞춥니다.
- 장점:
- 신뢰성 100%: 테스트 코드(JUnit)를 실행하여 통과한 요청과 응답만을 기반으로 문서 조각(Snippet)을 생성합니다. 로직이 바뀌어 테스트가 깨지면 빌드 자체가 실패하므로, 문서가 생성되지 않습니다. 따라서 문서가 나왔다는 것은 곧 “검증된 API“임을 보장합니다.
- 프로덕션 코드 오염 없음: 문서화 로직이 전부
src/test패키지 안에 격리되어 있어, 실제 서비스 코드는 매우 깔끔하게 유지됩니다.
- 단점:
- 진입 장벽: 테스트 코드를 반드시 작성해야 하며, Gradle/Maven 설정과 AsciiDoc 문법을 익히는 데 러닝 커브가 존재합니다.
- 정적인 문서: 기본적으로 HTML 파일로 생성되므로 Swagger처럼 즉석에서 API를 호출해보는 기능은 제공하지 않습니다. (단, 최근에는 RestDocs로 OpenAPI Spec을 뽑아내어 Swagger UI에 띄우는 하이브리드 방식도 존재합니다.)
결론적으로, “빠른 프로토타이핑이나 내부 어드민 툴“이라면 Swagger가 유리하지만, “외부에 공개되는 API나 장기적인 유지보수가 필요한 핵심 서비스“라면 Spring RestDocs가 압도적으로 유리합니다.
2. Spring RestDocs 아키텍처 및 동작 원리
Spring RestDocs의 핵심은 MockMvc (또는 WebTestClient, RestAssured) 테스트의 실행 결과로부터 문서 조각을 추출하는 것입니다.
- 테스트 실행: 개발자가 JUnit 테스트를 실행하여 API 요청을 수행합니다.
- 인터셉트 및 캡처: Spring RestDocs 프레임워크가 테스트 실행 도중의 HTTP 요청과 응답을 가로채서(Intercept), 헤더, 본문, 상태 코드 등을 분석합니다.
- 스니펫(Snippet) 생성: 분석된 정보를 바탕으로
.adoc확장자를 가진 문서 조각 파일들(http-request.adoc,http-response.adoc,request-fields.adoc등)을build/generated-snippets경로에 자동 생성합니다. - 문서 조합: 개발자가 미리 작성해 둔 메인 문서 파일(
index.adoc)에 생성된 스니펫들을include명령어로 불러와 하나의 완벽한 문서로 조립합니다. - HTML 변환:
Asciidoctor플러그인이.adoc파일을 파싱 하여 최종적인 HTML 파일로 변환하고, 이를 정적 리소스 경로(static/docs)로 복사하여 배포합니다.
3. 실전 구축 가이드 (Gradle + JUnit 5 + Spring Boot 3)
이제 실제로 구축해 보겠습니다. Spring Boot 3.x 버전과 Gradle Kotlin DSL을 기준으로 작성되었습니다.
Step 1: build.gradle 설정
RestDocs의 가장 큰 진입 장벽은 복잡한 빌드 설정입니다. 아래 설정을 그대로 사용하시면 됩니다. asciidoctor 플러그인과 copyDocument 태스크 간의 의존성 설정이 핵심입니다.
Kotlin
plugins {
java
id("org.springframework.boot") version "3.2.0"
id("io.spring.dependency-management") version "1.1.4"
id("org.asciidoctor.jvm.convert") version "3.3.2" // Asciidoctor 플러그인
}
val asciidoctorExt: Configuration by configurations.creating // 스니펫 생성을 위한 설정
dependencies {
// ... 기본 의존성 ...
// RestDocs 의존성
testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
asciidoctorExt("org.springframework.restdocs:spring-restdocs-asciidoctor")
}
val snippetsDir = file("build/generated-snippets") // 스니펫이 생성될 경로
tasks.test {
outputs.dir(snippetsDir)
useJUnitPlatform()
}
tasks.asciidoctor {
inputs.dir(snippetsDir)
configurations("asciidoctorExt")
dependsOn(tasks.test) // 문서 생성 전에 반드시 테스트가 먼저 수행되어야 함
// index.adoc을 제외한 다른 adoc 파일은 html로 변환하지 않음 (선택 사항)
sources {
include("**/index.adoc")
}
// Gradle 빌드 시 snippetsDir에 있는 조각들을 참조할 수 있도록 설정
baseDirFollowsSourceFile()
}
// 생성된 HTML 문서를 jar 파일 안에 패키징하기 위한 설정
tasks.bootJar {
dependsOn(tasks.asciidoctor)
from("${tasks.asciidoctor.get().outputDir}") {
into("static/docs")
}
}
Step 2: 테스트 서포트 클래스 (RestDocsSupport) 작성
모든 컨트롤러 테스트에서 중복되는 RestDocs 설정 코드를 제거하기 위해 추상 클래스를 만듭니다. @AutoConfigureRestDocs를 사용할 수도 있지만, 커스터마이징(예: JSON 이쁘게 출력하기)을 위해 직접 설정하는 방식을 선호합니다.
Java
@Import(RestDocsConfiguration.class)
@ExtendWith(RestDocumentationExtension.class) // JUnit 5 확장
public abstract class ControllerTestSupport {
protected MockMvc mockMvc;
@BeforeEach
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(prettyPrint()) // 요청 JSON 포맷팅
.withResponseDefaults(prettyPrint()) // 응답 JSON 포맷팅
)
.build();
}
}
Step 3: 실제 테스트 코드 작성 및 문서화
이제 컨트롤러 테스트를 작성합니다. mockMvc.perform 뒤에 이어지는 .andDo(document(…)) 부분이 바로 문서를 생성하는 핵심 로직입니다.
Java
@DisplayName("회원 단건 조회 API 테스트")
@Test
void getMember() throws Exception {
// given
Long memberId = 1L;
// when & then
mockMvc.perform(get("/api/members/{memberId}", memberId)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
// 문서화 시작
.andDo(document("member-get", // 식별자 (폴더명)
pathParameters(
parameterWithName("memberId").description("회원 ID")
),
responseFields(
fieldWithPath("id").type(JsonFieldType.NUMBER).description("회원 ID"),
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("name").type(JsonFieldType.STRING).description("이름"),
fieldWithPath("createdAt").type(JsonFieldType.STRING).description("가입일")
)
));
}
위 테스트를 실행하고 나면 build/generated-snippets/member-get 폴더 안에 http-request.adoc, response-fields.adoc 등이 생성된 것을 확인할 수 있습니다. 만약 컨트롤러가 반환하는 필드가 추가되거나 삭제되었는데 responseFields를 수정하지 않는다면, 테스트는 즉시 실패합니다. 이것이 바로 RestDocs가 보장하는 무결성입니다.
Step 4: AsciiDoc 문서 작성 (index.adoc)
마지막으로 src/docs/asciidoc/index.adoc 파일을 만들고 생성된 스니펫들을 배치합니다.
AsciiDoc
= My Service API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:
== 회원(Member) API
=== 회원 단건 조회
==== 요청
include::{snippets}/member-get/http-request.adoc[]
include::{snippets}/member-get/path-parameters.adoc[]
==== 응답
include::{snippets}/member-get/http-response.adoc[]
include::{snippets}/member-get/response-fields.adoc[]
이제 ./gradlew bootJar를 실행하면, 테스트가 수행되고 -> 스니펫이 생성되고 -> index.html로 변환되어 -> JAR 파일 안에 쏙 들어갑니다. 서버를 띄우고 http://localhost:8080/docs/index.html에 접속하면 우리가 만든 아름다운 API 문서를 볼 수 있습니다.
3. 결론
지금까지 Spring RestDocs와 Swagger를 비교하고, RestDocs를 활용한 자동화 환경을 구축해 보았습니다.
Swagger가 주는 편리함도 분명 매력적이지만, 장기적인 관점에서 서비스의 품질을 유지하고 팀원 간의 신뢰 비용을 줄이는 데에는 Spring RestDocs가 압도적으로 훌륭한 선택지입니다. “테스트 없이는 문서도 없다”는 강제성은 초기에는 불편하게 느껴질 수 있으나, 곧 “문서가 있으니 이 API는 안전하다”는 강력한 믿음으로 바뀔 것입니다.
또한, 잘 작성된 테스트 코드는 그 자체로 훌륭한 예제 코드이자 스펙 문서 역할을 합니다. Spring RestDocs 도입은 단순히 문서를 만드는 도구를 바꾸는 것이 아니라, 개발 팀의 문화를 ‘동작하는 문서‘를 지향하는 TDD 문화로 업그레이드하는 중요한 계기가 될 것입니다. 오늘 당장 여러분의 프로젝트에 적용하여, 더 이상 거짓말하지 않는 정직한 API 문서를 만들어 보시기 바랍니다.