처음에는 분명 RestDocs가 혁신적인 API 문서라고 느꼈는데, 갈수록 의문이 들기시작했다.
분명 단점들이 존재했다. 그렇다고 swagger를 사용하기엔 프로덕트 코드에 어노테이션들이 덕지덕지 붙어있는 건 싫어서 더 나은 방법이 없을까 고민하던 찰나.
정말 극한의 효율과 클라이언트에게 친화적인 API 문서를 만들 수 있겠다는 생각이 들었고, 그 이유는 바로 restdocs + swagger를 합쳐서 사용하는 것 이였다. docs로 만들어진 문서를 swagger로 변환하여 사용하면...(혁신이다)
정말 극한의 문서를 만들고 싶은 여러분께 극한의 API 문서를 자동화해서 만드는 법을 이번 포스팅에서 다루어보겠다.
restdocs + swagger 문서를 제작하는 로직은 아래와 같다.
로직은 아주 간단하다. 사실 구현 자체도 어렵지 않은데, 필자의 경우 정말 많은 삽질을 하였다. 여러 포스팅이 있었지만 제대로 되지 않거나, build 과정에서 패키지가 충돌하는 오류가 발생해 내 머리에도 오류가 발생할 뻔 했지만 코드를 새로 짜면서 gradle과 코드를 구성하여 성공하였다.
코드를 보면서 구현하면 간단하기에 천천히 따라와보자 (발생했던 Exception도 포스팅에 작성해보겠다)
// 1. Import 추가
import com.sun.security.ntlm.Server
import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
import org.springframework.boot.gradle.tasks.bundling.BootJar
// 2. buildscript 추가
buildscript {
ext {
restdocsApiSpecVersion = '0.17.1'
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
// 3. openAPI 플러그인 추가
id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
// 4. swaggerUI 플러그인 추가
id 'org.hidetake.swagger.generator' version '2.18.2'
}
group = 'study.project'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
// 5. 생성된 API 스펙이 어느 위치에 있는지 지정
swaggerSources {
sample {
setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
}
}
// 6. openapi3 스펙 생성시 설정 정보
openapi3 {
servers = [
{ url = "http://배포중인 주소" },
{ url = "http://localhost:8080" }
]
title = "API 문서"
description = "RestDocsWithSwagger Docs"
version = "0.0.1"
format = "yaml"
}
dependencies {
.
.
.
// 7. RestDocs 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// 8. openAPI3 추가
testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
// 9. SwaggerUI 추가
swaggerUI 'org.webjars:swagger-ui:4.11.1'
}
tasks.named('test') {
useJUnitPlatform()
}
// 10. openapi3가 먼저 실행 - doFrist를 통한 Header 설정 (글에서 자세하게 설명)
tasks.withType(GenerateSwaggerUI) {
dependsOn 'openapi3'
doFirst {
def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yaml")
def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
" - APIKey: [] # Apply the security scheme here"
swaggerUIFile.append securitySchemesContent
}
}
// 11. 생성된 openapi3 스펙을 기반으로 SwaggerUISample 생성 및 static/docs 패키지에 복사
bootJar {
dependsOn generateSwaggerUISample
from("${generateSwaggerUISample.outputDir}") {
into 'static/docs'
}
}
주석을 통해 간단하게 설명했지만, 해당 build를 이해하는 것이 중요하기에 하나씩 살펴보자.
🔍 해당 부분을 통해 openAPI3가 먼저 생성되고, 생성된 스펙에 구문을 추가하여 Authorization에 토큰을 추가할 수 있게 한다.
기존 docs에서 requestHeader()로 작성한 테스트코드를 Header로 인식을 하여 입력칸이 생기기는 하나, 실제로 보내지진 않는다. 결국 Authorization과 같은 헤더 타입에 토큰을 넣기 위해선 스펙안에 토큰을 넣는 코드를 따로 정의 해주어야 한다.
doFirst {
def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yaml")
def securitySchemesContent = " securitySchemes:\n" + \
" APIKey:\n" + \
" type: apiKey\n" + \
" name: Authorization\n" + \
" in: header\n" + \
"security:\n" +
" - APIKey: []"
swaggerUIFile.append securitySchemesContent
}
해당 로직의 과정 중에서 doFrist 메서드를 통해 생성된 openAPI3 스펙에 마지막 구문에 헤더 관련 설정을 추가해준다. 이는 openAPI3 스펙에 준수하여 모든 API에 Authorization 헤더에 토큰을 넣을 수 있게 설정해준다.
docs + swagger를 구현을 했지만 헤더에 토큰을 넣지 못하는 상황이 발생해 수많은 삽질을 통해 해당 방법을 터득했다,, 정작 swagger를 사용하는데 API를 요청해보지 못하는 건 큰 리스크이기에 해결이 꼭 필요했다.
⚠️ 참고로 doLast로 하면 해당 구문을 추가하기 전에 SwaggerUI가 생성되기에 적용이 안되니 꼭 doFirst 메서드를 사용하자.
문서를 자동화하는 만큼 build.gradle 에서 정의해야하는 스크립트들이 중요하니 해당 부분만 잘해도 완성한 것 이나 다름없다.
저번 포스팅에서 다루었던 restdocs 테스트 코드를 그대로 사용해도 된다.
- docs 테스트 코드 작성시 document (변경전)
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
- docs + swagger 테스트 코드 작성시 document(변경후)
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
위와 같이 기존 docs에서 import 구문을 변경하는 것 만으로 docs+swagger를 적용할 수 있지만, swagger의 정보와 설정을 변경하기 위해서 아래와 같이 테스트 코드를 리팩터링 할 수 있다.
@DisplayName("소셜 로그인 API")
@Test
void socialLogin() throws Exception {
// given
// 생략..
// when // then
mockMvc.perform(
RestDocumentationRequestBuilders.post("/auth/signin")
.param("code", "JKWHNF2CA78acSW6AUw7cvxWsxzaAWVNKR34SAA0AZ")
.param("platform", "KAKAO")
)
.andDo(print())
.andExpect(status().isOk())
.andDo(document("socialLogin",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(ResourceSnippetParameters.builder()
.tag("User API")
.summary("소셜 로그인 API")
.formParameters(
parameterWithName("code").description("발급받은 인가코드"),
parameterWithName("platform").description("플랫폼 : 'GOOGLE' / 'KAKAO' "))
.responseFields(
fieldWithPath("code").type(NUMBER).description("상태 코드"),
fieldWithPath("message").type(STRING).description("상태 메세지"),
fieldWithPath("data.userId").type(NUMBER).description("유저 ID"),
fieldWithPath("data.email").type(STRING).description("유저 이메일"),
fieldWithPath("data.nickName").type(STRING).description("유저 닉네임"),
fieldWithPath("data.profileImageUrl").type(STRING).description("유저 프로필 이미지"),
fieldWithPath("data.accessToken").type(STRING).description("액세스 토큰"),
fieldWithPath("data.refreshToken").type(STRING).description("리프레쉬 토큰"))
.requestSchema(Schema.schema("FormParameter-socialLogin"))
.responseSchema(Schema.schema("UserResponse.Login"))
.build())));
restdocs 작성에 대해서는 전편에 자세하게 다루고 있으니 전편을 참고하도록 하자 ➡️ 전편 보러가기
원래의 경우 request, response에 관한 부분을 document 내부에서 바로 작성해주었지만, openAPI3 스펙을 자세하게 정의하기 위해 resource로 한번 감싼 이후에 tag, summary와 같은 옵션들을 사용해 커스텀 해줄 수 있다.
이정도만 해도 openAPI3 스펙을 더 깔끔하게 만들 수 있다.
프로젝트 루트 경로에서 아래 커맨드를 통해 빌드를 해주고 서버를 켜보도록 하자.
./gradlew build
cd build/libs
java -jar {생성된 jar 파일}
그리고 localhost:8080/docs/index.html로 접근하게 되면...!
restdocs와 swagger의 합작품이 탄생된다.. 광기와 극한의 효율을 추구하는 사람이 만들어버린 괴물같은 API 문서(?)다.
사용법은 swagger와 동일하며 Authorization에 JWT와 같은 토큰이 필요하다면 requestHeaders로 테스트 문서를 작성했다면, 입력칸이 하나 생기는데 해당 입력칸은 건드리지 않고 API 우측상단에 있는 자물쇠버튼을 클릭해서 토큰을 넣어주면 된다.
자물쇠를 누르면 아래와 같은 창이 뜨는데
만약 Bearer 토큰이라면 Bearer {JWT Token} 과 같이 넣어주면 된다.
ex) Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTY5Mjk2OTMzNH0.RkW_m_hucGooYnRHSjtRSfEJ4Us3Bjl4IReD2NzFBGHVpBCUxxjwJOza1huhxjvsTMEiIIcKizUwvXqjV2wdzA
이러면 진짜 끄읏-!
사실 2번의 경우 애초에 문서를 생성하지를 못하다보니 문제가 컸었고, 3번의 경우 문서는 구현이 됐는데 restdocs + swagger를 적용한 이유가 restdocs의 안정성과 swagger의 클라이언트 친화적인 문서를 만들기 위해서였는데 swagger의 장점인 API 요청을 못한다는 것은 적용의 의미가 사라지기 때문에 어떻게든 헤더에 토큰을 넣기 위해 발버둥 치다가 성공했습니다 ...🥹
삽질도 많이 했고 결국 성공도 했습니다.. 극한의 API 문서를 만드는데 성공했기에 API 문서를 약간 정복해버린 느낌이 드네요,, restdocs와 swagger 장점만 뽑아서 합쳐놓은 문서이기에 아마 API 문서는 이렇게 정착할 것 같습니다.
restdocs은 테스트를 기반으로 하다보니 API 문서의 신뢰도가 높았지만 조각을 직접 넣어줘야 한다는 점, API를 요청해볼 수 없다는 점, 디자인이 좋지 않다는 점,, 그리고 swagger의 경우 프로덕트 코드에 어노테이션이 많이 붙는다는 점 이러한 단점들을 다 없애고 장점만 남겼다는게 왤케 뿌듯한지 모르겠습니다..
아무튼 여러분들도 restdocs + swagger 꼭 쓰세요 🙇♂️
참조
https://github.com/ePages-de/restdocs-api-spec
https://thalals.tistory.com/433
https://jwkim96.tistory.com/274
큰 도움 받았습니다. 감사합니다.
혹시 지금도 requestHeader값을 지정해 줄 수 없는걸까요?ㅠ 공식 문서 찾아보고 있는데 아직 찾지 못해서요
restdocs-api-spec의 인증 부분을 추가하기 위해 검색하던 중 선생님의 글을 읽게 되었습니다.
gradle의 securitySchemesContent 부분 잘 읽었습니다. 감사합니다.
글에서 말씀하신대로 적용해도 전혀 문제가 없습니다만,
restdocs-api-spec 문서를 좀 더 파서 정식적으로 써보자는 생각으로 연구한 결과 알려드립니다.
문서에서는 헤더만 잘 입력하면 자동으로 생성해준다고 되어 있습니다.
https://github.com/ePages-de/restdocs-api-spec?tab=readme-ov-file#security-definitions-in-openapi
restdocs-api-spec inspects the AUTHORIZATION header of a request for a JWT token. Also the a HTTP basic authorization header is discovered and documented. If such a token is found the scopes are extracted and added to the resource.json snippet.
코드를 보면 실제로 JWT 토큰을 디코딩까지 해보고 openapi security 관련 로직을 실행합니다.
JwtSecurityHandler.kt
SecurityRequirementsHandler.kt
ResourceSnippet.kt
SecuritySchemeGenerator.kt
JWT로 예를들면,
테스트 코드의 헤더에
헤더명을 HttpHeaders.AUTHORIZATION 으로 입력하고,
헤더값을 Bearer 를 적은 뒤 실제 디코딩 가능한 JWT 토큰을 입력해주면 문서에 Authorize 버튼이 생성됩니다.
(jwt.io 사이트에서 예제로 나와있는 JWT를 사용해도 무방합니다.)
RestDocumentationRequestBuilders.post("/v1/foo/bar")
.header(HttpHeaders.AUTHORIZATION, "Bearer {디코딩 가능한 JWT 토큰을 입력}")
.content(reqDtoJson)
.contentType(MediaType.APPLICATION_JSON)
헤더값 앞부분만 Bearer 대신 Basic 을 적어주시면 베이직 토큰 인증도 가능합니다.
Oauth 관련 인증은 아래 링크를 참고하시면 될 것 같습니다.
https://github.com/ePages-de/restdocs-api-spec?tab=readme-ov-file#openapi-301-1
덕분에 좋은 내용 잘 보고 갑니다.
정말 감사합니다.