[Spring] restdocs + swagger ui 같이사용하기 (restdocs 문서 자동화)
✨ api 문서화 정리 글
오느른, 오늘은,, 오늘우리는,,, Spring Restdocs 를 사용해 test 코드로 Ascciidoc 문서조각을 모으고,, 모아서 만든 adoc 문서를 또 편집하고.. html 로 변환하고 이 일련의 귀찮은 과정을 거쳐야하는 Spring Rest Docs 의 단점을 커버하기 위해
Swagger 와 restdocs 를 결합해보겠습니다 ! 👏🏻👏🏻👏🏻
사실 시작은 이러했습니다.
현재 사내에서는 Swagger 만을 사용하고 있기 때문에 프로덕션코드에 API 문서를 위한 코드가 존재하는게 지저분해서 현재 하고있는 토이프로젝트에는 RestDocs 를 적용해 보았습니다.
하지만, 아래처럼 깔금하게 원하는 형식대로 문서화가 진행되지 않았고 (제가 잘 몰라서 그런것일지돟ㅎㅎ), 또 docs 문서화를 위한 테스트코드 작성 까지는 좋은데 → 그 후에 다시 index.adoc 문서를 추가 작업해야하는게 너무 귀찮습니다 !!!!!
😂 그리고 막상 API 가 늘어나니까 보기도 굉장히 불편해 이쁜 Swagger UI 가 그리웠습니다..
그러다 지인이 좋은 인사이트를 주었다!!
바로 레츠고.
1. 📗 Swagger UI
- Swagger 는 Swagger 와 Swagger UI 로 나뉘어집니다.
- Swagger로 코드를 작성하면 OpenAPI 코드가 작성되고 이를 Swagger-UI 로 시각화 해주는식으로 동작합니다.
- 즉, 꼭 Swagger를 사용하지 않아도 Swagger-UI 만을 사용할 수 있습니다.
그럼 이제, restdocs 로 작성된 문서를 Swagger-UI 가 읽을 수 있도록 변환해주는 설정을 먼저 해줘야겠죠?
2. ⚙️ restdocs-api-spec
- https://github.com/ePages-de/restdocs-api-spec
- ePages-de/restdocs-api-spec : Spring Rest Docs 의 테스트코드를 활용해 OpenAPI(Swagger 3.0) 의 결과를 만들어주는 오픈소스 라이브러리 입니다.
- Spring Rest Docs 의 스펙을 따라가기위해 "MockMvc, WebTestClient, RestAssured" 모두 지원한다고 합니다.
사용방법도 간단하여 기존의 RestDocs로 작성된 문서도 거의 건들이지 않고도 수정할 수 있습니다.
단순하게 API 문서 생성부으 ㅣ구현체를 이 라이브러리에서 제공하는 구현체로 바꾸기만 해도 문서는 만들어집니다!
전체적인 구조는 아래 사진과 같습니다.
1) 설정하기
- 기존의 adocs 문서를 만들기위한 의존성과 코드들은 전부 삭제했습니다.
- 먼저 "rest-cods-api-spec" 플러그인을 추가해주고 + 밑에 MockMvc를 추가해줄 때 버전을 맞추기위해 버전만 변수로 추출했습니다.
buildscript {
ext {
restdocsApiSpecVersion = '0.17.1' // restdocsApiSpecVersion 버전 변수 설정
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
//restdocs-api-spec → restdocs extension 포함함
id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
//swagger generator 플러그인 추가
id 'org.hidetake.swagger.generator' version '2.18.2'
}
group = 'com.tht'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
targetCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
//spring rest docs
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
//restdocs-api-spec 의존성 추가
testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
...
}
test {
// outputs.dir snippetsDir
useJUnitPlatform()
}
// Task 및 설정 추가
// GenerateSwaggerUI 태스크가, openapi3 task 를 의존하도록 설정
tasks.withType(GenerateSwaggerUI) {
dependsOn 'openapi3'
//기존 파일 삭제했다가, build 에 출력한 json 정적파일 복사 (안해도 됨 → local 확인용)
delete file('src/main/resources/static/docs/')
copy {
from "build/resources/main/static/docs"
into "src/main/resources/static/docs/"
}
}
//openAPI 작성 자동화를 위해 패키징 전에 openapi3 테스크 선실행을 유발
bootJar{
dependsOn(':openapi3')
}
openapi3 {
server = "http://tht-talk.store"
title = "THT API 문서"
description = "Spring REST Docs with SwaggerUI."
version = "0.0.1"
outputFileNamePrefix = 'open-api-3.0.1'
format = 'json'
// /static/docs/open-api-3.0.1.json 생성 → jar 파일만 배포할 예정이기에 build 에 출력
outputDirectory = 'build/resources/main/static/docs'
}
openapi3 설정
openapi3 로 Open API 3 스펙을 만들때 필요한 부가정보들 입니다.
- setServer(...) : 서버 주소를 설정합니다. API 요청 보내기 기능에서 이 주소가 사용됩니다.
- title : API 문서의 제목
- description : API 문서의 설명
- version : API 문서의 버전
- format : API 문서 출력 포멧(default : JSON)
- outputDirectory : format 형식으로 변환할 파일을 저장할 디렉토리 경로를 설정할 수 있습니다.
이렇게하면 설정은 끝입니다. 이제 테스트코드를 작성해 봅시다!
2) 테스트코드 작성
변경을 최소화하고 싶으시다면 패키지 설정에 딱 1줄만 바꾸면 됩니다 !
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
↓
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
구현체를 이렇게만 바꿔도 동작은 하지만 조금 더 깔끔하게 작성하기 위해서, 나머지 테스트 코드 작성방법도 정리해두겠습니다.
공통부분 추상화
- 모든 Controller 테스트에 들어가는 부분을 추상화한 클래스 입니다.
- restdocs 를 사용할 때와 다르게 @AutoConfigureMockMvc 와 @AutoConfigureRestDocs 두 어노테이션 꼭 작성해야한다고 합니다.
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
public abstract class ControllerTestConfig {
@Autowired
protected WebApplicationContext ctx;
@Autowired
protected ObjectMapper objectMapper;
protected MockMvc mockMvc;
@BeforeEach
void setUp(final RestDocumentationContextProvider restDocumentation) {
mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
.apply(documentationConfiguration(restDocumentation))
.addFilters(new CharacterEncodingFilter("UTF-8", true))
.alwaysDo(print())
.build();
}
}
Controller Code
- Get 방식이면 응답값이 List 형식이기 때문에 [].idx 이런식으로 접근을했습니다.
- 단일 객체라면 idx 바로 사용하면 되고.
- List 를 멤버변수를 가지고 있다면 key.idx 이런식으로 접근하면 될겁니다
import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
import static org.mockito.Mockito.when;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.epages.restdocs.apispec.Schema;
import com.tht.api.app.facade.interest.InterestFacade;
import com.tht.api.app.facade.interest.response.InterestResponse;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@WebMvcTest(InterestController.class)
class InterestControllerTest extends ControllerTestConfig {
private static final String DEFAULT_URL = "/interests";
@MockBean
InterestFacade interestFacade;
@Test
@DisplayName("관심사 목록 리스트 조회")
void getInterestAll() throws Exception {
//given
when(interestFacade.getInterestList()).thenReturn(
List.of(new InterestResponse(1, "이상형 명칭", "emoji code"))
);
//then
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get(DEFAULT_URL)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
).andDo(
MockMvcRestDocumentationWrapper.document("interest-docs",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(
ResourceSnippetParameters.builder()
.description("관심사 목록 리스트 조회")
.requestFields()
.responseFields(
fieldWithPath("[].idx").description("idx"),
fieldWithPath("[].name").description("이상형 명칭"),
fieldWithPath("[].emojiCode").description("이모티콘 코드")
)
.build()
)
)
);
resultActions.andExpect(MockMvcResultMatchers.status().isOk());
}
}
public record InterestResponse (Integer idx, String name, String emojiCode){}
이렇게 작성하면 gradle 에서 설정해든 output 디렉토리에 json 형식의 파일이 생성됩니다 !
json 파일을 열어서 확인해보면 Swagger UI 에서 인식하는 기본 샘플 형식과 같은 형식으로 데이터가 추출되었음을 확인할 수 있습니다.
이제 Swagger UI를 적용해봅니다.
3. 👍 Swagger-UI 적용하기
많은 분들이 Swagger 서버를 도커로 따로 띄어서 사용하시던데, 저는 MSA 환경이 아니기 때문에 Spring 에 의존성을 추가하여 바로 사용해보도록 하겠습니다.
사실상 의존성 하나만 추가해도 Swagger UI 를 사용할 수 있습니다.
이게 Spring boot 3.0 이라 그런지 java 17 이라 그런지.. open-api 가장 최신버전을 사용해서 그런지 모르겠지만 하단의 의존성을 추가해야지만 Swagger 화면이 정상적으로 나왔습니다.
// Swagger ui - https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui
// implementation 'org.springdoc:springdoc-openapi-ui:1.7.0' → 이거 안됨
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
의존성을 추가하고 다시 빌드해서 👉 "http://localhost:8080/docs/swagger-ui/index.html" 경로에 들어가면 Swagger 기본 디폴트 페이지가 뜸을 확인할 수 있습니다.
하지만, 기본 페이지가 아닌 저희가 만들어준 Json 파일을 바라보도록 swagger default 를 수정해줘야겠져
📌 Swagger default Json 경로 수정
- application.yaml 파일에서 간단한 설정을 할 수 있습니다.
- swagger-ui.url : swagger-ui가 읽은 파일 경로
- swagger-ui.path : swagger-ui 띄울 url 변경 → 이러면 "http://localhost:8080/docs/docs/swagger" 를 통해 문서를 불러올 수 있습니다.
springdoc:
default-consumes-media-type: application/json;charset=UTF-8
default-produces-media-type: application/json;charset=UTF-8
swagger-ui:
url: /docs/open-api-3.0.1.json
path: /docs/swagger
이렇게 설정을 하면 짠! 하고 설정한 restdocs 로 만든 문서화가 만들어졌습니다.
📌 Swagger UI 이쁘게 적용하기
- 다좋은데 기본적으로 설정을 하지 않으면 기본 Swagger 만 사용했을 때처럼 이쁘지가 않습니다.
- 위의 "이상형 목록", "관심사 목록", "유저" 도 저렇게 한글이 아닌 영어로 나올텐데
- 이 주제와 아래의 Schema 를 Response 형식 이름을 이쁘게 바꿔보겠습니다
간단하게 Resource Build 설정에서 값을 변경할 수 있습니다.
//then
ResultActions resultActions = mockMvc.perform(
RestDocumentationRequestBuilders.get(DEFAULT_URL)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
).andDo(
MockMvcRestDocumentationWrapper.document("ideal-types-docs",
preprocessRequest(prettyPrint()),
preprocessResponse(prettyPrint()),
resource(
ResourceSnippetParameters.builder()
.tag("이상형 목록")
.description("이상형 목록 리스트 조회")
.requestFields()
.responseFields(
fieldWithPath("[].idx").description("idx"),
fieldWithPath("[].name").description("이상형 명칭"),
fieldWithPath("[].emojiCode").description("이모티콘 코드")
)
.responseSchema(Schema.schema("IdealTypeResponse"))
.build()
)
)
);
- ResourceSnippetParameters.builder()
직접 작성할 API 정보를 추가할 때 사용됩니다. - tag(String)
위의 이미지에서 “테스트"에 해당하는 부분으로 태그를 통해 여러API를 묶을 수 있습니다. - summary(String)
API의 요청 URL 옆에 들어가는 제목입니다. - description(String)
API 세부 정보로 들어갔을 때 존재하는 설명입니다. - requestSchema(Schema)
해당 API를 호출할때 사용한 본문과 매핑되는 객체의 이름입니다. - responseSchema(Schema)
해당 API의 반환 값의 본문과 매핑되는 객체의 이름입니다.
쨘! 잘나오네요
이렇게해서 Restdocs로 테스트 코드만 작성하면 추가적인 문서 편집없이
Swagger UI 로 표출되도록 설정을 끝마쳤습니다.
너무 만족스러운 작업이네요
그럼 20000 ~
*참고
- https://jwkim96.tistory.com/274
- https://shirohoo.github.io/backend/test/2021-07-17-swagger-rest-docs/#-%EB%B0%9C%EC%83%81
- 이건 이쁘게 만들기 - https://yongc.tistory.com/18