Spring Boot
프로젝트에 Swagger UI
적용하기REST Docs
를 사용해서 OAS(Open API Specification)
만들기OAS
생성 후 Swagger UI
와 연동API 문서는 백엔드 API 서버를 개발하여 이를 사용하는 클라이언트와 소통하는데 사용하는 문서 입니다.
우테코에서 진행한 팀 프로젝트에서 API 문서의 사용자는 안드로이드 분야 개발자 팀원인 것이죠!
안드로이드 팀원 모두 Swagger
를 이전에 미션을 통해 사용해 본 경험이 있었고, 다른 프로젝트를 했을 때 사용했던 API 문서보다 훨씬 편리했다고 합니다.
API 문서 또한 하나의 서비스이고 어떠한 서비스든 사용자가 편해야 사용하기 좋습니다.
API 문서 사용자인 안드로이드 팀원의 의견은 아래와 같았습니다.
Swagger
를 사용하니 개발 서버에 직접 요청을 테스트해볼 수 있어서 편리했어요.Postman
과 같은 다른 프로그램으로 직접 요청 후 응답 값을 보고 추측해서 개발 했어요.요약해보면 직접 요청할 수 있는 것과 API가 변경되면 즉시 반영 이 핵심 요구사항 이었습니다.
백엔드 팀원들의 경험으로 API 문서를 어떤 것으로 만들지에 선택지는 아래와 같았습니다.
Swagger
REST Docs
Postman
Swagger 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 UI
REST 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 ⭐
queryParameters
responseFields
메서드를 정확하게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 접속 시 아래와 같이 보인다면 성공입니다!