Swagger & RestDocs

공호진·2022년 12월 26일
0
post-thumbnail
post-custom-banner
  1. 특징 및 장단점
  2. Spring Rest Docs 과 Swagger 조합하기
    1. 2.1 Spring REST Docs 및 restdocs-api-spec 설정
    2. 테스트 코드에 Spring REST Docs 녹이기
  3. Swagger-ui를 통해, Spring Rest Docs 단점 보완하기

1. 특징 및 장단점

Spring Rest DocsSwagger
장점제품코드에 영향 없다.API 를 테스트 해 볼수 있는 화면을 제공한다.
테스트가 성공해야 문서작성된다.
깔끔 명료한 문서를 만들수 있다.
단점적용하기 어렵다.
- 테스트 코드가 필수제품코드에 어노테이션 추가해야한다.
제품코드와 동기화가 안될수 있다.

참조 : https://techblog.woowahan.com/2597/

💡 만약 Rest Docs의 **문서화 장점**과 Swagger의 **UI를 통한 테스트 기능**을 조합한다면? 
	 - 현재 팀은 Test code 문화를 가지고 있기 때문에, Rest Docs을 안 쓸 이유가 없음

2. Spring Rest Docs 과 Swagger 조합하기

Spring REST Docs 의 Flow는 아래 이미지 점선 부분의 상단 구조를 가지고 있다.

이러한 구조에 Swagger의 장점 덧붙이기 위해서 플러그인의 추가(빨간 박스)가 필요하다.

출처 : https://jwkim96.tistory.com/274

따라서 두 프레임워크의 장점을 살리면서, 조화롭게 쓰기 위한 핵심 Plugin은 다음과 같다.

  • com.epages.restdocs-api-spec
    • Spring REST Docs 을 확장하는 역할
    • 테스트 코드(Mock,Rest)의 entrypoint 의 정보(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` 파일을 생성하는 역할
    • 참조 : https://github.com/ePages-de/restdocs-api-spec
  • org.hidetake.swagger.generator
    • openApi 명세서를 통해, SwaggerUI 제공

2.1 Spring REST Docs 및 restdocs-api-spec 설정

Spring MVC Test 또는 WebTestClient로 생성된 자동 생성 스니펫(snippets)을 결합하여, RESTful 서비스를 정확하고 읽기 쉬운 문서로 생성

  1. RestDocs, 테스트 관련 dependencies 설정
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 사용도 가능 )
  1. restdocs-api-spec 설정
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 태스크 선실행을 유발
  
}

- outputDirectory 옵션의 default value : build/api-spec


2.2 테스트 코드에 Spring REST Docs 녹이기

자세한 테스트 코드 예제 소스는 해당 깃헙 참조(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 API 문서관리 시 단점은 수정되는 소스와 문서가 동기화가 안된다는 점이다.
    하지만 테스트코드를 바탕으로 문서를 만드는 REST DOCS 은 위의 문제점을 해소할 수 있다.

3. Swagger-ui를 통해, Spring Rest Docs 단점 보완하기.

Rest Docs는 제공하는 UI가 다소 부족하다는 단점이 있지만, Swagger-generator(org.hidetake.swagger.generator)를 통해 보완할 수 있다.

org.hidetake.swagger.generator 플러그인은 위와 같이 정의된 open-api-3.0.1.json 정의서를 바탕으로 swagger-ui에 활용할 수 있게 해준다.

  1. 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"))
    }
}
  • 1.1, 1.2
    • swagger-ui sources를 프로젝트에 추가해, plugins이 사용할 수 있도록 설정
  • 1.3
    • openapi3 에 의해 생성되는 OpenAPI 명세서 파일의 경로를 설정
❯ ./gradlew generateSwaggerUI

위와 같은 커맨드를 실행시킬 시 다음과 결과물이 생성

build 시, swagger 리소스 생성 자동화하기

물론, 위와 같은 방법으로, 수동적으로 생성할 수 있지만, 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 에 정의한 명세를 바탕으로, 정보 제공


profile
내일 더 나은 개발자가 되기 위해, 오늘을 기록합니다
post-custom-banner

0개의 댓글