
Spring Boot 프로젝트에 Swagger UI 적용하기
REST Docs 를 사용해서 OAS(Open API Specification) 만들기
OAS 생성 후 Swagger UI 와 연동
API 문서는 백엔드 API 서버를 개발하여 이를 사용하는 클라이언트와 소통하는데 사용하는 문서 입니다.
우테코에서 진행한 팀 프로젝트에서 API 문서의 사용자는 안드로이드 분야 개발자 팀원인 것이죠!
안드로이드 팀원 모두 Swagger 를 이전에 미션을 통해 사용해 본 경험이 있었고, 다른 프로젝트를 했을 때 사용했던 API 문서보다 훨씬 편리했다고 합니다.
API 문서 또한 하나의 서비스이고 어떠한 서비스든 사용자가 편해야 사용하기 좋습니다.
API 문서 사용자인 안드로이드 팀원의 의견은 아래와 같았습니다.
Swagger 를 사용하니 개발 서버에 직접 요청을 테스트해볼 수 있어서 편리했어요.Postman 과 같은 다른 프로그램으로 직접 요청 후 응답 값을 보고 추측해서 개발 했어요.요약해보면 직접 요청할 수 있는 것과 API가 변경되면 즉시 반영 이 핵심 요구사항 이었습니다.
백엔드 팀원들의 경험으로 API 문서를 어떤 것으로 만들지에 선택지는 아래와 같았습니다.
SwaggerREST DocsPostmanSwagger UI + REST Docs단순 Swagger 는 라이브러리 하나만 설치한 후 Controller 에 특정 어노테이션을 추가하는 것 만드로 편하게 API 문서를 만들 수 있는 강력한 도구입니다.
하지만 어노테이션 추가는 메인 로직이 담진 소스 코드를 더럽힌다는 단점이 있는데요.
이 또한 아래 코드 처럼 인터페이스를 두어 해결이 가능하긴 합니다. (옆 팀이 알려준 방법이에요!)
@Tag(name = "Member API")
@SecurityRequirement(name = "Authorization")
public interface MemberControllerSwagger {
@Operation(
summary = "회원 추가",
responses = @ApiResponse(responseCode = "201", description = "회원 추가 성공")
)
@ErrorCode500
ResponseEntity<Void> save(@Parameter(hidden = true) String authorization);
}
하지만 모든 컨트롤러 마다 파일을 추가로 생성해 줘야 하며 같이 따라다닌 다는 점에서 포기했었습니다.

REST Docs 하나만 사용하면 많은 adoc 확장자의 스니펫 파일을 만들어 주어 이를 기반으로 API 문서를 html 로 생성해줍니다.
테스트 코드로 작성하기 때문에 소스 코드의 더럽힘 없이 API 문서를 만들 수 있어서 좋아 보였지만 결국 실제 요청을 해보려면 안드로이드 팀원은 웹 페이지에서가 아닌 다른 요청 프로그램을 사용해야 했기에 Swagger 쪽으로 여전히 마음이 많이 기울어져 있었습니다.

팀 내 여러 백엔드 크루 중 한 크루가 이전 미션 때 Postman 을 사용했다고 알려주었으며 API 개발이 끝나고 Postman 으로 요청하면 나온 응답 값을 저장해두어 이를 기반으로 API 문서를 만들어 준다고 하였습니다.
Swagger 처럼 직접 요청을 할 수 있다는 점이 매력적이었습니다.
하지만 여전히 API가 변경되었을 때 문서 동기화 면에서 뾰족한 해결 방안이 없었습니다.
또한 큰 걸림돌은 아니지만 해당 팀원이 Export 하여 알려준 문서에 들어가서 테스트해보려 하니 Postman 에 로그인 해야하는 점도 있었습니다.

Swagger 는 자체적으로 OAS 도 제작해주며 이를 Swagger UI 가 바라보게 하여 동작하는 원리입니다.
OAS (Open API Specification)
토스페이먼츠에서 설명은 굉장히 잘 해주어서 꼭 읽어보시는 것을 추천드립니다!
https://docs.tosspayments.com/resources/glossary/oas
Swagger 가 OAS 를 생성할 때 컨트롤러에 어노테이션을 붙여야 했었는데 이 부분은 REST Docs 의 테스트 코드 작성으로 바꿔버린다면 원했던 아래 2가지를 모두 충족시킬 수 있었습니다.
Swagger UIREST Docs 테스트 코드Rest Assured 라는 API 테스트 도구를 사용하는데 Spring Context 를 띄운 뒤 요청 및 응답을 받는 라이브러리 입니다.REST Docs 는 미리 정의된 요청과 응답에 일치한지 확인한 후 맞을 경우에만 테스트를 통과시키게 되므로 API가 변경되었을 때 테스트 실패로 API 문서를 고치게 될 것입니다.따라서 REST Docs 로 OAS 인 yaml 파일을 먼저 생성한 뒤 이를 정적 파일 위치인 resources/static 으로 옮겨 컴파일 하면 이후 웹 서버가 뜰 때 Swagger UI 에서 읽고 사용할 수 있게 될 것입니다.
설치해야할 라이브러리는 org.springdoc:springdoc-openapi-starter-webmvc-ui 입니다.
충돌
어디서
springfox의 Swagger UI 설치하라고도 알려줬던 것 같아요.
하지만 설치 시 다른 라이브러리랑 충돌 날 수도 있다고 들었던 것 같습니다.
업데이트도 꾸준히 되는org.springdoc의 라이브러리를 사용하시는 것을 추천드립니다!
설치 전 많이 사용하는 버전은 어느 것인지 먼저 Maven Repository 에서 확인합니다.
https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-starter-webmvc-ui

2.2.0 버전을 가장 많이 사용하지만 최신 버전을 아예 기피하지 않으므로 해당 버전을 사용하기로 하였습니다.
먼저 build.gradle 의 dependencies 에 아래 코드를 추가합니다.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
코끼리 새로고침을 해줍니다.

application.yaml 에 아래 내용을 추가합니다. (application.properties 를 사용할 경우 절절히 . 으로 변경해서 사용하시면 됩니다.)
springdoc:
enable-default-api-docs: false
api-docs:
path: /openapi3.yaml
springdoc.enable-default-api-docs 기본 OAS 사용 여부springdoc.api-docs.path 직접 만든 OAS Endpoint 경로Spring Boot 를 실행한 후 아래 링크로 접속 시 사진과 같이 보인다면 성공입니다.
http://localhost:8080/swagger-ui/index.html

이제 저 openapi3.yaml 이라는 파일을 만들 차례입니다!
ePages 라는 곳에서 restdocs-api-spec 라이브러리를 만들었고 해당 문서로는 아래 Github 링크가 있습니다.
https://github.com/ePages-de/restdocs-api-spec
해당 깃허브 리드미의 Getting started 를 보니 Spring Boot 에 따른 버전 선택이 명시되어 있네요!
처음 설치 때 해당 부분을 보지 않고 다른 레퍼런스만 따라하다가 시간 낭비를 했었습니다.
Maven Repository 의 최신 버전 Usages 도 살펴보고 문서도 꼼꼼히 살펴 시간 저처럼 시간 낭비를 하지 않으셨으면 좋겠어요!

결론적으로 안정적이여 보이는 0.18.2 버전을 선택하였습니다.

testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
restdocs-api-spec-mockmvc 라이브러리도 추가하였는데 처음엔 restdocs-api-spec-restassured 에 포함되어 할 필요 없을 줄 알았는데 OAS 를 생성하려면 기본적으로 필요한 베이스 라이브러리였습니다.
Rest Assured
또한, 저희 팀은 API 테스트 도구로
Mock을 사용하지 않고 직접 API 요청을 보내는Rest Assured를 사용하였기에 관련 라이브러리 2개가 추가되었는데Mock을 사용하실 경우 제일 아랫 줄만 추가하시면 될 것이며, 이 글이 아닌ePages깃허브 공식 문서를 따라하셔서 OAS 를 생성하시면 됩니다!
openapi3 라는 gradle task 의 추가적인 설정 값이 필요합니다.
build.gradle 에 dependencies 블럭 아래에 아래 블럭을 추가한 후 자신 상황에 맞게 수정해주세요.
servers API 요청을 보낼 서버 리스트 (첫번째가 메인)openapi3 {
servers = [{ url = "https://dev.pengcook.net" }, { url = "http://localhost:8080" }]
title = 'Pengcook API'
description = 'Pengcook API description'
version = '0.1.0'
format = 'yaml'
}

IntelliJ 사용자라면 우측에 Gradle 탭을 열어 documentation > openapi3 를 실행합니다../graldew openapi 를 입력합니다.이후 프로젝트 폴더에서 build > api-spec > openapi3.yaml 파일을 열어봅니다.
아래와 같이 보인다면 성공입니다!

아래 추상 클래스를 테스트 최상단에 위치시킵니다.
이는 RestAssured 라이브러리에 RequestSpecification 을 등록하여 REST Docs 와 연동하기 위함입니다.
⭐ import ⭐
앞으로 작성되는 코드들에 모두 일부러 구분지어
import구문을 추가해 두었습니다.
동일한 이름의 메서드가 여러 라이브러리에 존재하므로 테스트 코드만 복사 한 뒤 라이브러리는 직접import하며 어떤 것이 정확한 것인지 선택하시면서 외워두시길 바랍니다!
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration;
@ExtendWith(RestDocumentationExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class RestDocsSetting {
protected static final String DEFAULT_RESTDOCS_PATH = "{class_name}/{method_name}/";
protected RequestSpecification spec;
@LocalServerPort
int port;
@BeforeEach
void setUp() {
RestAssured.port = port;
}
@BeforeEach
void setUpRestDocs(RestDocumentationContextProvider restDocumentation) {
spec = new RequestSpecBuilder()
.addFilter(documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(prettyPrint(), modifyHeaders()
.remove("Host")
.remove("Content-Length")
)
.withResponseDefaults(prettyPrint(), modifyHeaders()
.remove("Transfer-Encoding")
.remove("Keep-Alive")
.remove("Date")
.remove("Connection")
.remove("Content-Length")
)
)
.setPort(port)
.build();
}
}
이제 제작한 API의 특정 Controller 테스트 파일에서 테스트 메서드를 아래와 같이 작성해주세요.
또한 새로운 컨트롤러 테스트 작성 시 문서화를 하려면 extends RestDocsSetting 하는 것과 RestAssured.given(spec) 을 꼭 기억해주시면 됩니다!
⭐ import ⭐
queryParametersresponseFields메서드를 정확하게import하시길 바랍니다!
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document;
import static org.hamcrest.Matchers.is;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
class RecipeControllerTest extends RestDocsSetting {
@Test
@WithLoginUser(email = "loki@pengcook.net")
@DisplayName("레시피 개요 목록을 조회한다.")
void readRecipes() {
RestAssured.given(spec).log().all()
.filter(document(DEFAULT_RESTDOCS_PATH,
"특정 페이지의 레시피 목록을 조회합니다.",
"레시피 조회 API",
queryParameters(
parameterWithName("pageNumber").description("페이지 번호"),
parameterWithName("pageSize").description("페이지 크기"),
parameterWithName("category").description("조회 카테고리").optional(),
parameterWithName("keyword").description("제목 또는 설명 검색 키워드").optional(),
parameterWithName("userId").description("작성자 아이디").optional()
),
responseFields(
fieldWithPath("[]").description("레시피 목록"),
fieldWithPath("[].recipeId").description("레시피 아이디"),
fieldWithPath("[].title").description("레시피 제목"),
fieldWithPath("[].author").description("작성자 정보"),
fieldWithPath("[].author.authorId").description("작성자 아이디"),
fieldWithPath("[].author.authorName").description("작성자 이름"),
fieldWithPath("[].author.authorImage").description("작성자 이미지"),
fieldWithPath("[].cookingTime").description("조리 시간"),
fieldWithPath("[].thumbnail").description("썸네일 이미지"),
fieldWithPath("[].difficulty").description("난이도"),
fieldWithPath("[].likeCount").description("좋아요 수"),
fieldWithPath("[].commentCount").description("댓글 수"),
fieldWithPath("[].description").description("레시피 설명"),
fieldWithPath("[].createdAt").description("레시피 생성일시"),
fieldWithPath("[].category").description("카테고리 목록"),
fieldWithPath("[].category[].categoryId").description("카테고리 아이디"),
fieldWithPath("[].category[].categoryName").description("카테고리 이름"),
fieldWithPath("[].ingredient").description("재료 목록"),
fieldWithPath("[].ingredient[].ingredientId").description("재료 아이디"),
fieldWithPath("[].ingredient[].ingredientName").description("재료 이름"),
fieldWithPath("[].ingredient[].requirement").description("재료 필수 여부"),
fieldWithPath("[].mine").description("조회자 작성여부")
)))
.queryParam("pageNumber", 0)
.queryParam("pageSize", 3)
.when()
.get("/recipes")
.then().log().all()
.body("size()", is(3));
}
}
이렇게 테스트 코드 작성이 끝났으며 openapi3 그래들 테스크를 실행하면 알아서 모든 테스트 코드도 실행되므로 문서가 알맞게 생성되는지 확인하시길 바랍니다.
기존에는 ./gradlew clean build 이런식으로 빌드파일 삭제 후 빌드 하는 것을 하셨을 거에요!
build task
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources
> Task :testClasses
> Task :test
> Task :check
> Task :build
그러면 bootJar 까지 알아서 실행되어 buid/lib/*.jar 파일이 생성됩니다.
하지만 위 과정 중 processResources 작업이 이루어 지기 전에 저희는 openapi3.yaml 을 resources/static 폴더안에 넣어두어야 합니다! 그래야 스프링이 실행될 때 Swagger UI 가 Endpoint /openapi3.yaml 로 접근해 내용을 읽고 사용할 수 있게 해주기 때문입니다.
개념 알고가기
processResources태스크는 이름에서도 알 수 있듯이resources/static안의 파일을 조합해주는 역할을 합니다.
하지만 다른 블로그들에서는 단순히bootJar태스크가 실행되기 전dependsOn으로openapi3를 하도록 하는데 이는 잘못된 방법입니다!
왜 안되지 하지 마시고Gradle태스크 출력 문구를 보고 정확히 이해하시는 것을 추천드립니다!
build.gradle 에 아래 새로운 태스크를 추가합니다.
openapi 블럭 아래에 두시면 됩니다.
dependOn 으로 OAS 를 먼저 생성한 후 build/api-spec/open3.yaml 파일을 resources/static 으로 옮기는 작업입니다.
tasks.register("copyOasToSwagger", Copy) {
dependsOn("openapi3")
from layout.buildDirectory.file("api-spec/openapi3.yaml").get()
into "src/main/resources/static"
}
위 태스크를 추가하고 코끼리를 새로고침 하면 우측 Gradle 패널에 아래와 같이 추가되어 있을거에요.
이것을 더블 클릭하여 실행해주세요!
IntelliJ 를 사용중이지 않다면 ./gradlew copyOasToSwagger 명령어를 실행해주세요!

copyOasToSwagger task
> Task :compileJava UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :compileTestJava UP-TO-DATE
> Task :processTestResources UP-TO-DATE
> Task :testClasses UP-TO-DATE
> Task :test UP-TO-DATE
> Task :check UP-TO-DATE
> Task :openapi3
> Task :copyOasToSwagger

위의 copyOasToSwagger task 대부분 UP-TO-DATE 인 것을 볼 수 있습니다.
이는 이전에 build 태스크를 실행했기 때문인데요, openapi3 가 실행하는 태스크들과 build 가 실행하는 태스크들이 곂치는 것이 많습니다!
마지막으로 아래 태스크 블럭을 추가합니다.
bootJar {
dependsOn("copyOasToSwagger")
}
이로써 최종적으로 bootJar 를 만들기 위해서는 아래 명령을 사용하면 됩니다.
./gradlew clean bootJar
명령어로 만드시면 순서대로 실행하며 기존 빌드 데이터를 지우고 컴파일 및 테스트를 진행한 후 OAS를 resources 로 옮긴 뒤 리소스를 bootJar 태스크는 리소스를 다시 처리한 뒤 최종 .jar 파일을 생성하게 됩니다.
빌드는 언제하나요?
bootJar태스크 이전에openapi3가 태스크가 동작하며 빌드와 테스트 모두 진행하게 됩니다.
따라서 굳이 build 라는 태스크를 추가적으로 진행할 필요가 없습니다.
빌드, 테스트, 배포 파일 생성을 분리하고 싶어요.
CD 진행 중 이 세 가지를 보통 분리하는데 간단하게 그냥
./gradlew build -x test처럼 하고싶지 않은 태스크는-x옵션으로 제외하거나 그냥 둬도 상관 없습니다.
태스크 진행 중UP-TO-DATE를 보게 되는데 그래들도 알아서 캐싱되어 이미 진행한 부분은 불필요한 실행을 또 하지 않기 때문입니다!
copyOasToSwagger 태스크 실행 이후 resources/static/openapi3.yaml 이 생겼을 경우 SpringApplication 을 실행합니다.
이후 http://localhost:8080/swagger-ui/index.html 접속 시 아래와 같이 보인다면 성공입니다!
