카테고리 없음

OAS - Swagger & Restdocs

freeParksey 2023. 10. 16. 19:56

프런트와 소통을 위해 API Spec을 제공해줘야 했습니다. 이 과정에서 제공을 위해 고려했던 과정을 공유하고자 합니다.

순서는 가장 많이 사용하는 swagger와 spring rest docs에 대해서 설명하고 그 후 제가 선택했던 방법 순으로 이야기할 예정입니다.

 

 

 

OAS란?

openAPI Specification의 약자로 Rest API에 대한 문서화를 위한 규격입니다.

OAS와 관련하여 자세한 내용은 OpenAPI Specification v3.1.0에서 보면 될 거 같습니다.

 

Swagger는 이런 OAS 스펙을 구현한 프레임워크라고 생각하면 될 거 같습니다.

사실 OAS는 v3이전에는 swagger1, swagger2로 불렸었는데 SmartBear Software라는 회사가 개발하여 가지고 있었습니다. 그러다 Linux Foundation에 기부가 되면서 새로운 버전으로 OpenAPI Specification를 개발하면서 OAS3.0으로 변경이 되었습니다. 그러면서 이 OAS스펙에 맞게 새롭게 개발이 진행되고 있습니다.

 

 

Swagger

swagger는 크게 3개의 툴로 이루어져 있습니다. `Swagger Codegen`, `Swagger Editor`, `Swagger UI`로 되어 있습니다.

  • `Swagger Codegen`: OAS 명세(JSON/YAML)을 기반으로 Stub 코드를 만들어 내는 역할을 한다.
  • `Swagger Editor`: OAS 명세를 작성하는 하나의 툴
  • `Swagger UI`: OAS 명세를 통해 HTML API 문서로 렌더링 한다.

 

spring프레임워크에 특화된 라이브러리로 `springfox`와 `springdoc`가 있습니다. 이러한 라이브러리들은 Annotation을 통해 자동으로 API 명세를 만들어 Swager UI로 보여주는 역할을 합니다.

 

`springfox`의 경우 개발이 중단되기도 하였고 swagger2.0 스펙을 지원하다 보니, 최근에 개발된 `springdoc`가 oas3.0 스펙을 지원하는 것으로 비교해 봤을 때 부족한 부분이 있습니다. 따라서 지속적으로 개발이 진행되고 있고 oas3.0스펙을 지원하는 springdoc를 사용하여 예시를 보여줄 예정입니다.

 

springdoc의 경우 여러 라이브러리를 함께 지원합니다.

  • OAS 3.0
  • Spring boot v3 (Java17 & Jakarta EE 9)
  • JSR-303, @NotNull, @Min, @Max, and @Size
  • Swagger-ui
  • OAuth2

Springdoc 적용하기

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui'

 

설정 파일 추가

springdoc:
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    path: /api/docs
    disable-swagger-default-url: true
    display-request-duration: true
    operations-sorter: alpha

springdoc를 사용하여 API  문서를 생성할 때 기본적으로 적용이 되는 설정입니다.

요청과 응답에 대한 media type과 swagger-ui로 렌더링 할 때의 위치와 API 요청에 대한 실행시간, API 보이는 순서를 설정했습니다. 이 외에도 여러 설정을 적용할 수가 있습니다.

springdoc-openapi properties에서 참고하여 추가적으로 작성하면 될 거 같습니다.

 

 

빈등록

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
//                .components(new Components().addSecuritySchemes(
//                        "bererToken", 
//                        new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("JWT")))
                .info(new Info().title("Swagger with springdoc")
                        .description("springdoc을 통한 OAS구성")
                        .version("v0.0.1")
                        .license(new License().name("Apache 2.0").url("http://springdoc.org")))
                .externalDocs(new ExternalDocumentation()
                        .description("문서")
                        .url("https://springshop.wiki.github.org/docs"));
    }

    @Bean
    public GroupedOpenApi mainOpenApi() {
        String packagesToscan[] = {"com.example.oauth.controller"};
        return GroupedOpenApi.builder().group("main").packagesToScan(packagesToscan)
                .build();
    }

    @Bean
    public GroupedOpenApi adminOpenApi() {
        String packagesToscan[] = {"com.example.oauth.admin.controller"};
        return GroupedOpenApi.builder().group("admin").packagesToScan(packagesToscan)
                .build();
    }

    @Bean
    public GroupedOpenApi memberOpenApi() {
        String packagesToscan[] = {"com.example.oauth.member.controller"};
        return GroupedOpenApi.builder().group("users").packagesToScan(packagesToscan)
                .build();
    }

    @Bean
    public GroupedOpenApi shortOpenApi() {
        String packagesToscan[] = {"com.example.oauth.shorted.controller"};
        return GroupedOpenApi.builder().group("short").packagesToScan(packagesToscan)
                .build();
    }
}

먼저 OpenAPI문서를 생성하기 위해 위와 같이 bean을 등록해야 했습니다. 

 

 

메인 header와 같은 느낌이라 생각하시면 될 거 같습니다. 따라서 헤더에 들어갈 여러 내용을 넣을 수 있습니다.

 

group openApi는 우측 상단의 group을 나누어 선택할 수 있게 하고, 그룹에 맞게 controller를 지정하면 그 내부에서 사용할 수 있습니다.

 

API문서 추가

@SecurityScheme(
        name="bearerAuth",
        type= SecuritySchemeType.HTTP,
        scheme = "Bearer",
        bearerFormat = "JWT"
)
@Tag(name = "Admin", description = "관리자 API")
@RestController
@RequestMapping("/admin")
public class AdminController {

    @Operation(summary = "테스트1", description = "관리자 테스트 1",
            security = @SecurityRequirement(name="bearerAuth"))
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "관리자 테스트1 성공",
                    content = @Content(schema = @Schema(implementation = AdminResponse.class))),
            @ApiResponse(responseCode = "404", description = "Not Found",
                    content = @Content),
            @ApiResponse(responseCode = "401", description = "Authentication Failure",
                    content = @Content(schema = @Schema(hidden = true)))
    })
    @GetMapping
    public void adminTest1() {

    }

    @PostMapping
    public void adminTest2() {

    }
}
  • SecurityScheme: Bean등록에서 components의 BearerToken과 같다
    • 다만 위에는 전박적인 적용을 해주는 반면, 위 코드는 하나의 클래스에 대해 적용

 

또한 @ResponseBody나 @RequestBody를 통해 자동으로 API문서를 만들 때 추가해 줄 수 있다.

@GetMapping(value = "/oauth/google", produces = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    public void authorizationCode(@RequestParam("code") String code,
        @RequestParam("scope") String scope,
        @RequestParam("authuser") String user,
        @RequestParam("prompt") String prompt,
    HttpServletResponse response) throws IOException, URISyntaxException {
    
}

 

 

내가 느낀 Swagger의 장점과 단점

swagger를 많이 사용해 보지 않아도 딱 느낄 수 있었던 장점과 단점이 있었습니다. 

 

장점

  • 알록달록한 UI: 아무런 설정 없이 바로 완성된 UI를 제공해 주는 점이 좋았고, 눈에 확 들어왔습니다.
  • swagger-ui: 전항에서 ui의 디자인에 대해서 말씀드렸는데, 그 외에도 테스트 동작까지 같이 수행할 수 있다는 점도 하나의 장점이라 볼 수 있었습니다.
  • controller에서 명세를 간략하게 미리 볼 수 있었습니다.

 

단점

  • 보기 힘든 코드: 많은 어노테이션이 있고 그에 대한 여러 설명이나, 샘플까지 넣을 수 있다 보니 코드가 눈에 안 들어왔습니다.
  • springdoc을 위한 Bean객체, 즉 API문서화를 위한 Bean등록이 추가되어야 했습니다. 어떻게 보면 사소하지만 약간은 필요 없는 코드가 추가된 느낌을 받았습니다.
  • 지속적으로 

내가 관리해야 하는 코드가 늘어난 느낌이고, 코드에 집중이 안되고 분산되는 느낌도 받았습니다. controller에서 데이터가 어떤 형태로 들어오는지 더 명확하게 알 수 있고, 설명까지 넣다 보니 무슨 역할을 하는지 나중에 보더라도 쉽게 알 수 있었습니다. 다만 코드를 보려면 많은 어노테이션들이 걸리적거려 코드에 집중을 저하시켰던 부분이 있었습니다. 또한 처음에는 '어떤 데이터가 오고 가는지 한 번에 알 수 있어서 좋다'라고 생각했지만 이후 '근데 여기서 볼 필요가 있나? 어차피 테스트 코드에서 테스트 파라미터로 더 많은 예제를 볼 수 있는데 굳이?'라는 생각이 들었습니다.

 

Spring restdocs

Spring의 Rest Docs는 srping에서 작성된 테스트에서 생성된 snippet을 사용하여 만듭니다.

테스트를 통해 API 문서에 대해 문서와 실제 코드가 동기화되고 이 API 문서 자체가 올바르다는 신뢰성을 보장해 줍니다.

 

간단 용어

  • Asciidoctor: asciidoc을 HTML 등으로 변환하기 위한 프로세서
  • Snippet: 문서화에 필요한 문서 조각
    • 문서 조각 = `http-request`, `http-response`등을 나타낸다.

 

 

요구 사항

Spring Rest Docs를 사용하기 위해서는 최소 요구 사항이 있습니다. `Java17`과 `Spring Framework6`가 기반이 되어야 사용이 가능합니다.

 

 

Build Configuration

 

1. 아스키닥터 plugin 적용

plugins { 
	id "org.asciidoctor.jvm.convert" version "3.3.2"
}

 

2. Configurations: Asciidoctor를 확장하기 위한 의존성 구성

configurations {
	asciidoctorExt 
}

 

3. AsciidoctorExt 구성에 `spring-restdocs-asciidoctor`추가

dependencies {
	asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' 
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' 
}

Asciidoctor가 API문서를 생성하기 위해 snippet을 찾아볼 위치를 `build/generated-snippets`로 자동 지정이 됩니다.

또한 asciidoc의 장점 중 하나인 `::` operation블록을 사용할 수 있습니다.

 

사용하고자 하는 test 라이브러리에 따라 다른 라이브러리를 넣어주면 됩니다.

  • `MockMvc`: spring-restdocs-mockmvc
  • `WebClient`: spring-restdocs-webtestclient
  • `REST Assured`: spring-restdocs-restassured

 

5. Output Location: snippet들을 generate 하고 나온 결과물 위치

 

ext { 
	snippetsDir = file('build/generated-snippets')
}
test { 
	outputs.dir snippetsDir
}

 

6. asciidoctor가 수행할 task 설정

asciidoctor { 
	inputs.dir snippetsDir 
	configurations 'asciidoctorExt' 
	baseDirFollowsSourceFile()
    dependsOn test 
}

baseDirFollowsSourceFile()은 Asciidoctor가 변환 작업을 할 때 소스 파일의 디렉터리 구조 유지용

 

여기까지 하고 빌드를 하게 되면

 

빌드에 여러 스니펫들이 존재하는 것을 볼 수 있습니다.

 

여기서 하나 알고 가야 할 점은 snippet을 generate 하기 전에 해야 할 것이 있습니다.

바로. adoc파일을 만들어야 하는데 하나의 기준점이라 생각하면 될 거 같습니다.

따라서 `src/docs/asciidoc/`에 `*. adoc`파일을 하나 만들고 asciidoctor를 실행해 주면 html로 변환해 주는데, 우리의 목표는 직접 프로세스를 돌리는 것이 아니라 jar로 같이 묶는 것까지 하는 것이므로 추가적인 gradle설정을 보고 가겠습니다.

 

7. bootJar 패키징

bootJar {
	dependsOn(tasks.named("copyDocs"))
	from ("${asciidoctor.outputDir}/html5") {
		into 'static/docs'
	}
}

순서대로 bootJar가 asciidoctor를 의존하고, asciidoctor는 test를 의존합니다.

따라서 순서는 test > asciidoctor > bootJar순으로 수행됩니다.

이후 build를 하면, `./build/generated-snippets/html5`에 html파일이 생성되고, jar안의 `/static/docs/`폴더에 복사가 됩니다.

 

8. 파일 복사

tasks.register("copyDocs", Copy) {
	dependsOn asciidoctor
	from file("build/docs/asciidoc")
	into file("src/main/resources/static")
}

출력 결과물을 resoures.static으로 옮기기 위한 과정

 

 

9. /static/docs의 파일들 삭제

asciidoctor.doFirst {
	delete file('src/main/resources/static/docs')
}

지속적으로 파일들이 새로 가져오게 되는데, 이전 값들이 남아있으면 원하는 결과가 아닐 수 있기 때문에 삭제해 줍니다.

 

10. index.adoc파일 생성

= OAuth API 문서
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 3

== Google OAuth Client

=== Request

include::{snippets}/login-google/http-request.adoc[]

=== Response

include::{snippets}/login-google/http-response.adoc[]

위치는 위에서 언급했듯, `src/docs/asciidoc/index.adoc`을 만들어 주면 된다.

이후 bootJar로 빌드하도록 해놓았기 때문에

 

`./gradlew bootJar로 실행을 하게 되면 된다.

 

Rest Docs Test 작성

테스트는 JUnit 버전에 따라 다르게 작성할 수 있었습니다. 물론 상위호환이 되기 때문에 선택하여 사용하면 될 거 같습니다.

 

 

RestDocumentation생성 

Junit5.1에서는 `RestDocumentationExtension`을 어노테이션을 통해 합성하여 사용할 수 있도록 제공한다.

@RegisterExtension
final RestDocumentationExtension restDocumentationExtension = new RestDocumentationExtension();

 

 

JUnit5.0에서는 Extension을 클래스 어노테이션 `ExtendWith`로 제공한다.

@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(OAuthController.class)
class OAuthControllerTest {
}

와 같이 확장하여 사용하면 된다.

또한 기본 설정의 경우에도 

@BeforeEach
void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
        .apply(documentationConfiguration(restDocumentation)
            .uris()
                .withScheme("https")
                .withHost("docs.api.com")
                //.withPort(443) // default로 가능
            .and()
            .snippets()
                .withEncoding("UTF-8") // default는 JVM의 기본 Charset
                // .withTemplateFormat(TemplateFormats.markdown()) // 기본 asciidoctor
                /**
                 * private List<Snippet> defaultSnippets = new ArrayList<>(Arrays.asList(CliDocumentation.curlRequest(),
                 * 			CliDocumentation.httpieRequest(), HttpDocumentation.httpRequest(), HttpDocumentation.httpResponse(),
                 * 			PayloadDocumentation.requestBody(), PayloadDocumentation.responseBody()));
                 */
                 // .withDefaults()
                 .and()
                 .operationPreprocessors()
                     //.withRequestDefaults(modifyUris().scheme("https").host("docs.api.com").port(443)) // 해당 전처리기에만 적용
                     .withResponseDefaults(prettyPrint())
        )
        .build();
}

 

물론 이렇게 안 하고 `@AutoConfigureRestDocs`가 존재하지만, 어노테이션이다 보니 커스텀할 수 있는 부분이 더 적다.

 

@DisplayName("")
@Test
void redirect_login_success() throws Exception {
    // when
    ResultActions result = mockMvc.perform(
        get("/login/google")
            .accept(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    );

    // then
    result.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
        .andDo(
            document("login-google",
                responseHeaders(
                    headerWithName("Location").description("Google OAuth Login Page Redirect Url")
                ))
    );
}

이렇게 간단하게 했을 때

이런 식으로 나오는 걸 볼 수 있다.

 

추가적으로 snippet을 customizing 해야 하는 경우도 생기는데

만약 curl-request라는 snippet을 만든다면, `src/test/resources/org/springframework/restdocs/templates/asciidoctor`에 넣어주면 됩니다. 이후 테스트코드 입력 포맷은 `AbstractFieldsSnippet`을 상속받아서 사용할 수 있습니다.

 

Spring restdocs의 장점과 단점

장점

  • 테스트 코드를 기반으로. adoc파일을 만들기 때문에 테스트 코드가 강제가 된다.
  • API문서와 코드가 완전히 동기화된다.

단점

  • . adoc 형식의 문법을 알아야 했습니다.
  • 적용하는데 생각보다 많은 설정이 필요했고 사용되는 코드도 점점 복잡해졌습니다.

restdocs를 사용함으로써 기존 Swagger에서 느꼈던 제품코드에 집중할 수 없었던 문제를 해결할 수 있었습니다. 또한 이전에 테스트 코드를 거의 작성하지 않았던 프로젝트에서 안 좋은 기억이 있어서 다시 한번 테스트 코드의 중요성을 느껴 테스트 커버리지도 높이던 찰나에 테스트 코드를 통한 API문서 생성은 더 좋게 다가왔다.

 

하지만, 가장 아쉬웠던 부분은 적용해야 할 부분이 많다였습니다. 물론 커스터마이징 할 수 있다는 것 자체가 장점으로 받아 들어졌지만, 그만큼 snippets를 새로 만들고 테스트코드에 추가하고 등의 작업이 해야 할 일이 많다고 느껴진 것은 사실이었습니다. 그리고 ui의 단순함 또한 아쉽게 느껴졌는데, 기본 UI자체가 swagger-ui보다 단조로웠던 부분이 있습니다. 

 

내가 선택한 방법

사실 어떤 것이 더 좋냐는 위에서 나왔던 장단점들을 보며 선택을 할 수 있었지만, 현재 상황에 대해서도 생각을 해봐야 했습니다.

 

현 상황

저는 지금 정말 새로운 프로젝트를 들어가는 상황이었습니다. 따라서 API스펙은 주기적으로 바뀌는 상황입니다.

따라서 레거시방식이 없기 때문에 마이그레이션을 할 필요가 없었고 하나로 통일만 하면 되는 상태였습니다.

 

결정

사실, 처음에는 restdocs로 정하려 했는데, `document()`만 하게 된다면 기본적으로 테스트 코드에 맞게 자동으로 생성해 줄 수 있어서 스펙이 변경이 될 때마다 문서화를 위한 코드도 변경하지 않고 제품코드만 변경하면 될 거 같다 생각했습니다.

 

그런데 인프콘에서 어느 개발자분과 소중한 시간을 가지며 질의를 가질 수 있었는데, 거기서 하셨던 말씀이 제가 생각하지 못했던 부분을 집어주셔서 그 방식과 비슷하게 선택하게 되었습니다.

 

'거기서 나왔던 부분은 프로젝트 초기에 API스펙이 나와도 지속적으로 나오게 될 것이고, 그때마다 고치는 것은 사실 많은 리소스를 잡는데, 그렇기 때문에 우리는 swagger-ui과 yaml로만 작성을 한다'라고 했었습니다.

협업을 하는 입장에서 API Spec을 통해 프런트가 개발을 하게 될 건데 문제는 만약 `@Controller`클래스를 먼저 만들게 되면, 스펙에 따라 입력받는 dto와 반환 상태 등 여러 값이 변경되게 되고, 이에 따라 API문서화를 위한 어노테이션도 변경해야 합니다. 따라서 하나의 변경이 두 변경으로 이어지고 리소스가 많이 들어가게 됩니다.

 

따라서 처음에는 swagger-ui와 yaml을 통해 API Spec을 전달하고자 했고, 이후 테스트 코드를 작성하게 되면 그때 하나씩 spring rest docs와 swagger-ui를 함께 사용할 수 있게 변경하도록 결정했습니다.

 

 

1. OAS를 사용하여 작성한 YAML/JSON -> UI로 변경

yaml을 swagger ui로 변환하기 위해 크게 2가지 방식으로 많이 진행한다.

  1. swagger-ui의 static파일을 다운로드하여서 진행한다. (파일 다운)
  2. swagger-ui를 generate 해주는 라이브러리 사용 (관련 링크)

 

이 중에서 1번의 경우 버전업이 될 경우 계속 다운로드하여야 한다 생각이 돼서 2번으로 진행했습니다.

 

먼저 generate해주는 라이브러리는 gradle에서 작업을 수행할 수 있도록 도와주는데 이러한 `tasks`를 수행하기 위해 

plugins {
	...
	id 'org.hidetake.swagger.generator' version '2.19.2'
}

dependencies {
	...
	swaggerUI 'org.webjars:swagger-ui:3.52.5'
}

룰 넣어주었다.

 

이후 직접 만든 api를 넣어주었다.

 

openapi: 3.0.3
info:
  version: 1.0.0
  title: Simple API
  description: A simple API to illustrate OpenAPI concepts

servers:
  - url: https://example.io/v1

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
security:
  - BasicAuth: []

paths:
  /artists:
    get:
      description: Returns a list of artists
      #  ----- Added lines  ----------------------------------------
      parameters:
        - name: limit
          in: query
          description: Limits the number of items on a page
          schema:
            type: integer
        - name: offset
          in: query
          description: Specifies the page number of the artists to be displayed
          schema:
            type: integer
      #  ---- /Added lines  ----------------------------------------

      responses:
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
    post:
      responses:
        '201':
          description: "성공"

(예시를 가져왔습니다)

 

이 openapi를 ui로 변경하기 위해 

 

gradle에 `swaggerSources`를 넣어주면 된다.

swaggerSources {
	oauthtest {
		inputFile = file("src/main/resources/api.yaml")
	}
}

여기까지 작성 후 generateSwaggerUIOauthtest를 실행하게 되면

 ./gradlew generateSwaggerUIOauthtest

 

swagger-ui가 적용된 폴더를 확인할 수 있다. 

 

 

이후 build를 할 때에는 jar안에 넣어줘야 배포 시 접근이 가능하기 때문에, `swagger-ui-oauthtest`를  static폴더 내부로 옮겨야 합니다. 

tasks.register('copySwaggerUI') {
	dependsOn generateSwaggerUIOauthtest

	doLast {
		copy {
			def generateSwaggerUITask = tasks.named('generateSwaggerUIOauthtest', GenerateSwaggerUI).get()

			from "${generateSwaggerUITask.outputDir}"
			into "${project.buildDir}/resources/main/static/docs"
		}
	}
}

bootJar {
	dependsOn 'copySwaggerUI'
}

따라서 위와 같이 실행하게 되면 

정상적으로 동작하게 됩니다.

 

이렇게 yaml을 통해 작성을 하다. 추후 Controller에서 test코드의 작성과 함께 restdocs를 적용하게 되었습니다.

 

 

2. spring-restdocs를 yaml로 변경하여 ui로 변경할 수 있도록 적용하기

spring-restdocs의 결과물로는 `asciidoct`파일이 들어오게 된다. 하지만 swagger-ui에 적용하려면 `yaml`이나 `json`으로 만들어야 하기 때문에 변환을 해주는 과정이 필요하다 이를 도와주는 라이브러리가 있는데 `spring rest docs api specification`입니다.

 

이 라이브러리의 task들을 넣기 위해서 

plugins {
    ...
	id 'com.epages.restdocs-api-spec' version '0.18.2'
}

dependencies {
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2'
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

위와 같이 gradle을 넣어주었습니다.

 

이후 제공해 주는 tasks 또한 넣어주면 코드를 변환할 준비는 끝납니다.

openapi3 {
	setServer("http://localhost:8008")
	title = "Simple API2"
	description = "description OpenAPI concepts"
	version= "1.0.0"
	format = "yaml"
}

 

여기서 생각해야 할 부분은

 

test코드에서 파일을 만드는 과정을 `MockMvcRestDocumentationWrapper`의 document로 만들어야 한다는 것이다.

@Test
    void redirect_login_success() throws Exception {
        // when
        ResultActions result = mockMvc.perform(
                RestDocumentationRequestBuilders.get("/login/google")
                        .accept(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
        );

        // then
        result.andExpect(MockMvcResultMatchers.status().is3xxRedirection())
                .andDo(
                        MockMvcRestDocumentationWrapper.document("/login-google",
                                new ResourceSnippetParametersBuilder()
                                        .tag("Google login")
                                        .description("OAuth Login")
                                        .responseFields(
                                                PayloadDocumentation.fieldWithPath("email").description("이메일")
                                        )
                        )
                );
    }

 

이렇게 

gradlew openapi3

를 하게 되면,

위와 같이 yaml값을 만들어지는 것을 볼 수 있습니다.

 

이제 이 yaml을 jar파일에 넣어줘야 하기 때문에 swagger때와 같이 실행할 수 있도록 task의 의존성을 연결해 줘야 합니다.

 

bootJar {
	dependsOn 'copySwaggerUI'
}

tasks.withType(GenerateSwaggerUI) {
	dependsOn 'openapi3'
}

tasks.register('copySwaggerUI') {
	dependsOn 'generateSwaggerUIOauthtest', 'generateSwaggerUIOauthtest2'

	doLast {
		copy {
			def generateSwaggerUITask = tasks.named('generateSwaggerUIOauthtest', GenerateSwaggerUI).get()
			from "${generateSwaggerUITask.outputDir}"
			into "${project.buildDir}/resources/main/static/docs/v1"
		}

		copy {
			def generateSwaggerUITask2 = tasks.named("generateSwaggerUIOauthtest2", GenerateSwaggerUI).get()
			from "${generateSwaggerUITask2.outputDir}"
			into "${project.buildDir}/resources/main/static/docs/v2"
		}
	}
}

따라서 위와 같이 이전 yaml만 적용했을 때의 `generateSwaggerUIOauthtest`와 restdocs를 yaml로 변경한 `generateSwaggerUIOauthtest2`를 의존하고 실행을 하게 되면

 

이렇게 spring rest docs를 정상적으로 yaml로 변경하고 swagger-ui로 적용하는 것을 볼 수 있습니다.

 

 

결론

이렇게 실제로 yaml을 직접 swagger-ui로 적용시키고, spring-restdocs 또한 yaml로 변경 후 swagger-ui로 적용하면서 API를 보냈습니다. swagger만을 사용했을 때의 문제점은 spring-restdocs를 통해 보완이 가능했던 점을 겪을 수 있었고, restdocs만을 사용했을 때의 문제점 또한 순수 yaml과 swagger-ui를 통해 trade-off를 할 수 있었습니다.

 

막 개발이 시작되고 MVP기능에 따른 요구 사항이 계속 바뀌는 현시점에서 swagger만을 사용한다면, controller에 어노테이션이 들어가면서 지속적으로 docs를 최신화하고, 메인 기능을 볼 수 없다는 단점과 spring-restdocs를 사용하기에는 테스트 코드를 작성해야 하는데, 내가 테스트 코드를 완성하고 통과되기까지 API 문서를 공유 못한다는 점과 ui자체가 보기 힘들고, test기능도 없다는 단점이 있었기에 위와 같은 과정을 통해 수정했습니다.

 

위 방식이 best practice도 아닐 것이고, 각자에게 맞는 도구가 있기 때문에 원하는 방식으로 하면 될 것이라 생각합니다.

 

 

참고 문헌

Swagger:

 

 

Restdocs:

 

Swagger to Restdocs: