서론
의문점(Swagger와 Spring Rest Docs를 같이 사용할 수는 없는가?)
- 이전 블로그에서 우린 Spring Rest Docs는 도입하였다. 아래는 해당 블로그이다.
- Spring Rest Docs을 도입해보니 경험에서 오는 단점이 몇가지 존재하였다.
- Docs를 위한 테스트 코드 작성은 매우 좋은 것이라고 생각한다. 하지만 해당 API Docs들을 모으기 위해 adoc 파일에 Docs에 대한 추가 작업을 해야하는데 이 부분이 생각보다 너무 귀찮고 자주 까먹게 된다.
- UI가 진짜 안 예쁘다..😫 → UI가 안 예쁘면 손이 잘 안 가게되더라..(개인적인 생각)
![](https://velog.velcdn.com/images/devty/post/c8f8dbf3-c3c0-47fb-a48e-e14095e4c910/image.png)
- 그래서 찾아보니 Spring Rest Docs와 Swagger를 같이 사용할 수 있었다! 밑은 내가 참고 했던 컨퍼런스 영상이다.
- 해당 영상에서는 두개의 장점만을 뽑아서 사용하였다.
- Spring Rest Docs에 테스트 기반에 API Docs + Swaager에 UI를 합쳐서 사용하였다,
- 위에서 뽑은 단점으로 adoc 파일에 Docs를 합치는 방식이 아닌 Swagger에서 알아서 변환을 진행해준다.
본론
Spring Rest Docs + Swagger 도입기
com.epages.restdocs-api-spec
- Spring REST Docs의 결과물을 OpenAPI 3 스펙으로 변환한다.
org.hidetake.swagger.generator
- OpenAPI 3 스펙을 기반으로 SwaggerUI 생성한다. (HTML, CSS, JS)
- 전체 흐름은 아래와 같다.
![](https://velog.velcdn.com/images/devty/post/b40171aa-17c7-4bcb-802c-5ea64f2a893c/image.png)
- Spring Rest Docs는 Asciidoc로 바꿔지고 asciidoctor로 코드 조각을 모은 뒤 HTML로 변환시키는 작업을 진행한다.
- 동일하게 Spring Rest Docs를 사용하되 Open API 3로 변환한 뒤 Sawwger generator로 모은뒤 Swaager UI(HTML)로 변환시키는 작업을 진행한다.
Spring Rest Docs + Swagger 빌드 코드
- build.gradle
buildscript {
ext {
restdocsApiSpecVersion = '0.16.2'
}
}
plugins {
id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
id 'org.hidetake.swagger.generator' version '2.18.2'
}
swaggerSources {
sample {
setInputFile(file("${project.buildDir}/api-spec/openapi3.json"))
}
}
openapi3 {
setServer("http://localhost:8000")
title = "user-service Document"
description = "user- service Document Swagger UI."
version = "0.0.1"
format = "json"
}
dependencies {
testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"
swaggerUI 'org.webjars:swagger-ui:4.11.1'
}
tasks.named('test') {
useJUnitPlatform()
}
tasks.withType(GenerateSwaggerUI) {
dependsOn 'openapi3'
}
tasks.register('copySwaggerUI', Copy) {
dependsOn 'generateSwaggerUISample'
def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()
from("${generateSwaggerUISampleTask.outputDir}")
into("${project.buildDir}/resources/main/static/docs")
}
bootJar {
dependsOn 'copySwaggerUI'
}
}
- 앞전에 Spring Rest Docs에 대한 build.gradle 설정은 모두 지우면 된다.
- 나는 어떤걸 지웠는지 알려주기 위해 주석처리를 하였다.
- Spring Swagger(1):
restdocsApiSpecVersion
변수에 '0.16.2' 값을 할당한다.
- Spring Swagger(2): 플러그인을 설정한다.
com.epages.restdocs-api-spec
: Spring RestDocs와 OpenAPI 스펙 연결 플러그인이다.
org.hidetake.swagger.generator
: Swagger 문서 생성 플러그인이다.
- Spring Swagger(3): Swagger 문서 생성에 사용할 JSON 파일의 위치를 지정한다.
- Spring Swagger(4): Swagger 문서의 기본 정보(서버 주소, 제목, 설명, 버전, 포맷)를 설정한다.
- Spring Swagger(5): Swagger 관련 의존성을 추가한다.
restdocs-api-spec-mockmvc
: MockMvc 기반 테스트에서 API 문서 생성에 사용된다.
swagger-ui
: Swagger UI로 API 문서를 웹에서 볼 수 있게 한다.
- Spring Swagger(6):
GenerateSwaggerUI
타입 작업을 'openapi3' 작업에 종속시킨다.
- Spring Swagger(7):
copySwaggerUI
작업을 정의한다. 이 작업은 생성된 Swagger UI를 resources/main/static/docs
디렉터리로 복사한다.
- Spring Swagger(8):
bootJar
작업을 실행하기 전에 copySwaggerUI
작업을 실행한다.
- 단, Swagger는 빌드 프로세스의 일부로 포함하기에 Test를 돌렸을 땐 생성이 되지 않게 하기 위해 Test Task에 대해서 설정을 해주지 않았다.
Spring Rest Docs + Swagger 테스트 코드
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
@DisplayName("회원 가입 API 명세서")
public class UserRegisterControllerDocumentTest extends ControllerTest {
@Test
@DisplayName("회원 가입 API 성공 시나리오 문서화")
public void testRegisterUser() throws Exception {
RegisterUserRequest request = new RegisterUserRequest(
"testuser111",
"test111!!!",
"test111!!!",
"testNickname",
"010-1234-5678",
"testuser@example.com"
);
mockMvc.perform(RestDocumentationRequestBuilders.post("/users/register")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(request)))
.andExpect(status().isOk())
.andDo(print())
.andDo(MockMvcRestDocumentationWrapper.document("user/register-user",
requestFields(
PayloadDocumentation.fieldWithPath("username").type(JsonFieldType.STRING).description("사용자 아이디"),
PayloadDocumentation.fieldWithPath("password").type(JsonFieldType.STRING).description("사용자 비밀번호"),
PayloadDocumentation.fieldWithPath("confirmPassword").type(JsonFieldType.STRING).description("비밀번호 확인"),
PayloadDocumentation.fieldWithPath("nickname").type(JsonFieldType.STRING).description("사용자 닉네임"),
PayloadDocumentation.fieldWithPath("phone").type(JsonFieldType.STRING).description("사용자 전화번호"),
PayloadDocumentation.fieldWithPath("email").type(JsonFieldType.STRING).description("사용자 이메일 주소")
),
responseFields(
PayloadDocumentation.fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("작업 성공 여부"),
PayloadDocumentation.fieldWithPath("data").description("등록된 사용자 정보"),
PayloadDocumentation.fieldWithPath("data.username").type(JsonFieldType.STRING).description("사용자 아이디"),
PayloadDocumentation.fieldWithPath("data.nickname").type(JsonFieldType.STRING).description("사용자 닉네임"),
PayloadDocumentation.fieldWithPath("data.phone").type(JsonFieldType.STRING).description("사용자 전화번호"),
PayloadDocumentation.fieldWithPath("data.email").type(JsonFieldType.STRING).description("사용자 이메일 주소"),
PayloadDocumentation.fieldWithPath("data.role").type(JsonFieldType.STRING).description("사용자 역할"),
PayloadDocumentation.fieldWithPath("errorCode").type(JsonFieldType.STRING).description("에러 코드 (있는 경우)").optional()
)
));
}
}
- 우리가 앞전에 짰던 코드이다. 거의 다 동일한데 두가지만 변경하면 이 틀을 유지해서 돌릴 수 있다.
- MockMvcRequestBuilders → RestDocumentationRequestBuilders
org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
→ org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
- MockMvcRestDocumentation → MockMvcRestDocumentationWrapper
org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
→ com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
실행 되는 순서
> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :jar
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :openapi3
> Task :generateSwaggerUISample
> Task :copySwaggerUI
> Task :bootJar
> Task :assemble
> Task :build
- openapi3: OpenAPI 3 사양에 따른 API 문서를 생성한다.
- generateSwaggerUISample: Swagger UI를 위한 리소스를 생성한다.
- copySwaggerUI:
generateSwaggerUISample
task로 생성된 Swagger UI 리소스를 프로젝트의 build/resources/main/static/docs
디렉토리로 복사한다.
Spring Rest Docs + Swagger 테스트 실행
- 위 테스트 코드를 실행하면 아래와 같게 나온다.
![](https://velog.velcdn.com/images/devty/post/f07f1cb5-950d-4b60-b33f-b51d9a7cb3d4/image.png)
- openapi3.json → Spring Rest Docs의 문서 스니펫들을 기반으로 생성되는 OpenAPI 3.0 형식의 JSON 파일이다.
- index.html → Swagger UI는 OpenAPI 혹은 Swagger 형식의 API 문서를 기반으로 사용자 웹 페이지 형태로 API 문서를 만든다.
index.html
(Swagger UI 웹 페이지)는 앞서 생성된 openapi3.json
파일을 참조하여 만들어진 것이다.
Swaager UI
![](https://velog.velcdn.com/images/devty/post/3ff24ffb-d5f0-4cac-92bd-37388341003d/image.png)
- 그렇게 해서 생성 된 회원가입에 대한 API 명세서이다.
- 우리가 익히 알고 있는 Swagger UI라 간단하게 넘어가겠다.
결론
후기
- 이로서 내가 보기도 프론트 개발자가 보기도 기획자가 보기도 편한 API 명세서가 다 완성이 되었다.
- 검증이 완료되어 만들어진 API 명세서라 신뢰성이 100% 이상이다!
- 앞으로는 이제 데브옵스, 모니터링과 지금은 Common Module을 사용하는데 이걸 다른사람들도 편하게 사용하기 위해 오픈소스용 라이브러리를 하나 만들어볼 생각이다!