Spring Rest Docs | Swagger | |
---|---|---|
장점 | 제품코드에 영향 없다. | API 를 테스트 해 볼수 있는 화면을 제공한다. |
테스트가 성공해야 문서작성된다. | ||
깔끔 명료한 문서를 만들수 있다. | ||
단점 | 적용하기 어렵다. | |
- 테스트 코드가 필수 | 제품코드에 어노테이션 추가해야한다. | |
제품코드와 동기화가 안될수 있다. |
참조 : https://techblog.woowahan.com/2597/
💡 만약 Rest Docs의 **문서화 장점**과 Swagger의 **UI를 통한 테스트 기능**을 조합한다면?
- 현재 팀은 Test code 문화를 가지고 있기 때문에, Rest Docs을 안 쓸 이유가 없음
Spring REST Docs 의 Flow는 아래 이미지 점선 부분의 상단 구조를 가지고 있다.
이러한 구조에 Swagger의 장점 덧붙이기 위해서 플러그인의 추가(빨간 박스)가 필요하다.
출처 : https://jwkim96.tistory.com/274
따라서 두 프레임워크의 장점을 살리면서, 조화롭게 쓰기 위한 핵심 Plugin은 다음과 같다.
ResourceDocumentation
)와 문서화 형태로 정의된 리소스([ResourceSnippet](https://github.com/ePages-de/restdocs-api-spec/blob/master/restdocs-api-spec/src/main/kotlin/com/epages/restdocs/apispec/ResourceSnippet.kt))를 포함한
resource.json` 파일을 생성하는 역할Spring MVC Test 또는 WebTestClient로 생성된 자동 생성 스니펫(snippets)을 결합하여, RESTful 서비스를 정확하고 읽기 쉬운 문서로 생성
dependencies {
...
testImplementation(
'org.springframework.restdocs:spring-restdocs-webtestclient',
)
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude group: "junit", module: "junit"
}
...
}
spring-restdocs-webtestclient
기반으로 시작한다. (spring-restdocs-mockmvc
사용도 가능 )plugins {
id 'com.epages.restdocs-api-spec' version '0.16.0'
}
repositories { //2.1
mavenCentral()
}
dependencies {
...
testImplementation('com.epages:restdocs-api-spec-webtestclient:0.11.3') //2.2
...
}
openapi3 { // 2.3
server = 'http://localhost:8080'
title = 'Spring-Rest-Docs + Swagger-UI + Open-API-3.0.1'
description 'Spring-Rest-Docs의 장점과 Swagger의 장점을 모두 가져갈 수 있는 아키텍처를 구축한다'
version = '0.0.1'
outputFileNamePrefix = 'open-api-3.0.1'
format = 'json'
}
bootJar {
dependsOn(':openapi3') // OpenAPI 작성 자동화를 위해 패키징 전에 openapi3 태스크 선실행을 유발
}
com.epages:restdocs-api-spec
모듈을 처리하는 레포지토리 추가restdocs-api-spec-webtestclient
디펜던시 추가restdocs-api-spec-gradle-plugin
을 위한 설정 값
- outputDirectory
옵션의 default value : build/api-spec
자세한 테스트 코드 예제 소스는 해당 깃헙 참조(https://github.com/shirohoo/spring-rest-docs-examples/tree/main/epages-open-api)
@ExtendWith(RestDocumentationExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserApiControllerTest {
@Autowired
ObjectMapper mapper; // json string 변환을 위해 주입
WebTestClient webTestClient;
@BeforeEach
void setUp(WebApplicationContext context, RestDocumentationContextProvider restDocumentation) {
webTestClient = MockMvcWebTestClient.bindToApplicationContext(context) // 서블릿 컨테이너 바인딩
.configureClient() // 설정 추가
.filter(documentationConfiguration(restDocumentation)) // epages 문서 설정을 추가
.build();
}
@Test
void 사용자_정보를_생성한다() throws Exception {
// given
Mono<String> request = Mono.just(mapper.writeValueAsString(UserRequest.builder()
.name("홍길동")
.email("hong@email.com")
.phoneNumber("01012341234")
.build())
);
String expected = mapper.writeValueAsString(UserRequest.builder()
.id(1L)
.name("홍길동")
.email("hong@email.com")
.phoneNumber("01012341234")
.build());
// when
ResponseSpec exchange = webTestClient.post()
.uri("/api/v1/user")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.body(fromProducer(request, String.class))
.exchange();
// then
exchange.expectStatus().isOk() // 응답 상태코드가 200이면 통과
.expectBody().json(expected) // 응답 바디가 예상한 json string과 같으면 통과
.consumeWith(document("create", // 문서 작성 및 추가 검증 작업
preprocessRequest(prettyPrint()), // 문서에 json 출력을 이쁘게 해준다
preprocessResponse(prettyPrint()), // 문서에 json 출력을 이쁘게 해준다
resource(
ResourceSnippetParameters.builder()
.tag("User") // 문서에 표시될 태그
.summary("사용자 정보 생성") // 문서에 표시될 요약정보
.description("사용자 정보를 생성한다") // 문서에 표시될 상세정보
.requestSchema(schema("UserRequest")) // 문서에 표시될 요청객체 정보
.responseSchema(schema("UserResponse")) // 문서에 표시될 응답객체 정보
.requestFields( // 요청 field 검증 및 문서화
fieldWithPath("id").description("식별자"),
fieldWithPath("name").description("이름"),
fieldWithPath("email").description("이메일"),
fieldWithPath("phoneNumber").description("전화번호")
)
.responseFields( // 응답 field 검증 및 문서화
fieldWithPath("id").description("식별자"),
fieldWithPath("name").description("이름"),
fieldWithPath("email").description("이메일"),
fieldWithPath("phoneNumber").description("전화번호"),
fieldWithPath("createAt").description("등록일"),
fieldWithPath("updateAt").description("수정일")
)
.build()
)));
}
해당 테스트코드를 실행한 후, api 문서를 생성할 경우 다음과 같은 문서화된 파일을 생성할 수 있다.
{
"openapi" : "3.0.1",
"info" : {
"title" : "Spring-Rest-Docs + Swagger-UI + Open-API-3.0.1",
"description" : "Spring-Rest-Docs의 장점과 Swagger의 장점을 모두 가져갈 수 있는 아키텍처를 구축한다",
"version" : "0.0.1"
},
"servers" : [ {
"url" : "http://localhost:8080"
} ],
"tags" : [ ],
"paths" : {
"/api/v1/user" : {
"post" : {
"tags" : [ "User" ],
"summary" : "사용자 정보 생성",
"description" : "사용자 정보를 생성한다",
"operationId" : "create",
"requestBody" : {
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/UserRequest"
},
"examples" : {
"create" : {
"value" : "{\n \"id\" : null,\n \"name\" : \"홍길동\",\n \"email\" : \"hong@email.com\",\n \"phoneNumber\" : \"01012341234\"\n}"
}
}
}
}
},
"responses" : {
"200" : {
"description" : "200",
"content" : {
"application/json" : {
"schema" : {
"$ref" : "#/components/schemas/UserResponse"
},
"examples" : {
"create" : {
"value" : "{\n \"id\" : 1,\n \"name\" : \"홍길동\",\n \"email\" : \"hong@email.com\",\n \"phoneNumber\" : \"01012341234\",\n \"createAt\" : null,\n \"updateAt\" : null\n}"
}
}
}
}
}
}
}
}
},
"components" : {
"schemas" : {
"UserRequest" : {
"title" : "UserRequest",
"type" : "object",
"properties" : {
"phoneNumber" : {
"type" : "string",
"description" : "전화번호"
},
"name" : {
"type" : "string",
"description" : "이름"
},
"id" : {
"type" : "number",
"description" : "식별자"
},
"email" : {
"type" : "string",
"description" : "이메일"
}
}
},
"UserResponse" : {
"title" : "UserResponse",
"type" : "object",
"properties" : {
"phoneNumber" : {
"type" : "string",
"description" : "전화번호"
},
"name" : {
"type" : "string",
"description" : "이름"
},
"id" : {
"type" : "number",
"description" : "식별자"
},
"email" : {
"type" : "string",
"description" : "이메일"
}
}
}
}
}
}
수정되는 소스와 문서가 동기화
가 안된다는 점이다.Rest Docs는 제공하는 UI가 다소 부족하다는 단점이 있지만, Swagger-generator(org.hidetake.swagger.generator)를 통해 보완할 수 있다.
org.hidetake.swagger.generator
플러그인은 위와 같이 정의된 open-api-3.0.1.json
정의서를 바탕으로 swagger-ui에 활용할 수 있게 해준다.
build.gradle
파일에 swagger-ui 설정plugins {
...
id 'org.hidetake.swagger.generator' version '2.18.2' // 1.1
}
dependencies {
...
swaggerUI('org.webjars:swagger-ui:4.11.1') // 1.2
}
swaggerSources { // 1.3
sample {
setInputFile(file("${project.buildDir}/api-spec/open-api-3.0.1.json"))
}
}
openapi3
에 의해 생성되는 OpenAPI 명세서 파일
의 경로를 설정❯ ./gradlew generateSwaggerUI
위와 같은 커맨드를 실행시킬 시 다음과 결과물이 생성
물론, 위와 같은 방법으로, 수동적으로 생성할 수 있지만, build 시 OpenApi 및 Swagger Resource 생성 을 하나의 작업으로 묶는게 필요하다.
다음과 같은 설정을 통해 하나의 task로 생성하자.
//GenerateSwaggerUI 태스크가, openapi3 task 를 의존하도록 설정
tasks.withType(GenerateSwaggerUI) {
dependsOn 'openapi3'
}
// 생성된 SwaggerUI 를 jar 에 포함시키기 위해 build/resources 경로로 복사
tasks.register('copySwaggerUI', Copy) {
dependsOn 'generateSwaggerUISample'
def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()
from("${generateSwaggerUISampleTask.outputDir}")
into("${project.buildDir}/resources/main/static/docs")
}
다음과 커맨드 실행 후, http://localhost:8080/docs/index.html
웹브라우저 접속
❯ ./gradlew build bootRun
REST DOCS
에 정의한 명세를 바탕으로, 정보 제공