MSA Phase 7. API Document(2) - Spring Rest Docs + Swagger UI

devty·2023년 10월 2일
1

MSA

목록 보기
11/14
post-thumbnail

서론

의문점(Swagger와 Spring Rest Docs를 같이 사용할 수는 없는가?)

  • 이전 블로그에서 우린 Spring Rest Docs는 도입하였다. 아래는 해당 블로그이다.
  • Spring Rest Docs을 도입해보니 경험에서 오는 단점이 몇가지 존재하였다.
    1. Docs를 위한 테스트 코드 작성은 매우 좋은 것이라고 생각한다. 하지만 해당 API Docs들을 모으기 위해 adoc 파일에 Docs에 대한 추가 작업을 해야하는데 이 부분이 생각보다 너무 귀찮고 자주 까먹게 된다.
    2. UI가 진짜 안 예쁘다..😫 → UI가 안 예쁘면 손이 잘 안 가게되더라..(개인적인 생각)
  • 그래서 찾아보니 Spring Rest Docs와 Swagger를 같이 사용할 수 있었다! 밑은 내가 참고 했던 컨퍼런스 영상이다.
  • 해당 영상에서는 두개의 장점만을 뽑아서 사용하였다.
    • Spring Rest Docs에 테스트 기반에 API Docs + Swaager에 UI를 합쳐서 사용하였다,
    • 위에서 뽑은 단점으로 adoc 파일에 Docs를 합치는 방식이 아닌 Swagger에서 알아서 변환을 진행해준다.

본론

Spring Rest Docs + Swagger 도입기

  • com.epages.restdocs-api-spec
    • Spring REST Docs의 결과물을 OpenAPI 3 스펙으로 변환한다.
  • org.hidetake.swagger.generator
    • OpenAPI 3 스펙을 기반으로 SwaggerUI 생성한다. (HTML, CSS, JS)
  • 전체 흐름은 아래와 같다.
    • Spring Rest Docs는 Asciidoc로 바꿔지고 asciidoctor로 코드 조각을 모은 뒤 HTML로 변환시키는 작업을 진행한다.
    • 동일하게 Spring Rest Docs를 사용하되 Open API 3로 변환한 뒤 Sawwger generator로 모은뒤 Swaager UI(HTML)로 변환시키는 작업을 진행한다.

Spring Rest Docs + Swagger 빌드 코드

  • build.gradle
    // Spring Swagger(1)
    buildscript {
        ext {
            restdocsApiSpecVersion = '0.16.2'
        }
    }
    
    // Spring Swagger(2)
    plugins {
    //    id 'org.asciidoctor.jvm.convert' version '3.3.2'
        id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
        id 'org.hidetake.swagger.generator' version '2.18.2'
    }
    
    // Spring Swagger(3)
    swaggerSources {
        sample {
            setInputFile(file("${project.buildDir}/api-spec/openapi3.json"))
        }
    }
    
    // Spring Rest Docs
    //configurations {
    //    asciidoctorExt
    //}
    
    // Spring Swagger(4)
    openapi3 {
        setServer("http://localhost:8000") 
        title = "user-service Document"
        description = "user- service Document Swagger UI." 
        version = "0.0.1" 
        format = "json"
    }
    
    dependencies {
        // Spring Rest Docs
    //    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    //    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    
        // Spring Swagger(5)
        testImplementation "com.epages:restdocs-api-spec-mockmvc:${restdocsApiSpecVersion}"
        swaggerUI 'org.webjars:swagger-ui:4.11.1'
    }
    
    tasks.named('test') {
        useJUnitPlatform()
    //    outputs.dir snippetsDir // Spring Rest Docs
    }
    
    // Spring Swagger(6)
    tasks.withType(GenerateSwaggerUI) {
        dependsOn 'openapi3'
    }
    
    // Spring Swagger(7)
    tasks.register('copySwaggerUI', Copy) {
        dependsOn 'generateSwaggerUISample'
        def generateSwaggerUISampleTask = tasks.named('generateSwaggerUISample', GenerateSwaggerUI).get()
        from("${generateSwaggerUISampleTask.outputDir}")
        into("${project.buildDir}/resources/main/static/docs")
    }
    
    // Spring Swagger(8)
    bootJar {
        dependsOn 'copySwaggerUI'
    }
    
    // Spring Rest Docs
    //ext {
    //    snippetsDir = file('build/generated-snippets') // 아래서 사용할 변수
    //}
    
    // Spring Rest Docs
    //asciidoctor {
    //    inputs.dir snippetsDir
    //    configurations 'asciidoctorExt'
    //    dependsOn test
    //    doFirst {
    //        delete 'src/main/resources/static/docs'
    //    }
    //}
    
    // Spring Rest Docs
    //bootJar {
    //    dependsOn asciidoctor
    //    copy {
    //        from asciidoctor.outputDir
    //        into "src/main/resources/static/docs"
    //    }
    //}
    }
    • 앞전에 Spring Rest Docs에 대한 build.gradle 설정은 모두 지우면 된다.
      • 나는 어떤걸 지웠는지 알려주기 위해 주석처리를 하였다.
    • Spring Swagger(1): restdocsApiSpecVersion 변수에 '0.16.2' 값을 할당한다.
    • Spring Swagger(2): 플러그인을 설정한다.
      • com.epages.restdocs-api-spec: Spring RestDocs와 OpenAPI 스펙 연결 플러그인이다.
      • org.hidetake.swagger.generator: Swagger 문서 생성 플러그인이다.
    • Spring Swagger(3): Swagger 문서 생성에 사용할 JSON 파일의 위치를 지정한다.
    • Spring Swagger(4): Swagger 문서의 기본 정보(서버 주소, 제목, 설명, 버전, 포맷)를 설정한다.
    • Spring Swagger(5): Swagger 관련 의존성을 추가한다.
      • restdocs-api-spec-mockmvc: MockMvc 기반 테스트에서 API 문서 생성에 사용된다.
      • swagger-ui: Swagger UI로 API 문서를 웹에서 볼 수 있게 한다.
    • Spring Swagger(6): GenerateSwaggerUI 타입 작업을 'openapi3' 작업에 종속시킨다.
    • Spring Swagger(7): copySwaggerUI 작업을 정의한다. 이 작업은 생성된 Swagger UI를 resources/main/static/docs 디렉터리로 복사한다.
    • Spring Swagger(8): bootJar 작업을 실행하기 전에 copySwaggerUI 작업을 실행한다.
    • 단, Swagger는 빌드 프로세스의 일부로 포함하기에 Test를 돌렸을 땐 생성이 되지 않게 하기 위해 Test Task에 대해서 설정을 해주지 않았다.

Spring Rest Docs + Swagger 테스트 코드

import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;

@DisplayName("회원 가입 API 명세서")
public class UserRegisterControllerDocumentTest extends ControllerTest {

    @Test
    @DisplayName("회원 가입 API 성공 시나리오 문서화")
    public void testRegisterUser() throws Exception {
        RegisterUserRequest request = new RegisterUserRequest(
                "testuser111",
                "test111!!!",
                "test111!!!",
                "testNickname",
                "010-1234-5678",
                "testuser@example.com"
        );

        mockMvc.perform(RestDocumentationRequestBuilders.post("/users/register")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(request)))
                .andExpect(status().isOk())
                .andDo(print())
                .andDo(MockMvcRestDocumentationWrapper.document("user/register-user",
                        requestFields(
                                PayloadDocumentation.fieldWithPath("username").type(JsonFieldType.STRING).description("사용자 아이디"),
                                PayloadDocumentation.fieldWithPath("password").type(JsonFieldType.STRING).description("사용자 비밀번호"),
                                PayloadDocumentation.fieldWithPath("confirmPassword").type(JsonFieldType.STRING).description("비밀번호 확인"),
                                PayloadDocumentation.fieldWithPath("nickname").type(JsonFieldType.STRING).description("사용자 닉네임"),
                                PayloadDocumentation.fieldWithPath("phone").type(JsonFieldType.STRING).description("사용자 전화번호"),
                                PayloadDocumentation.fieldWithPath("email").type(JsonFieldType.STRING).description("사용자 이메일 주소")
                        ),
                        responseFields(
                                PayloadDocumentation.fieldWithPath("success").type(JsonFieldType.BOOLEAN).description("작업 성공 여부"),
                                PayloadDocumentation.fieldWithPath("data").description("등록된 사용자 정보"),
                                PayloadDocumentation.fieldWithPath("data.username").type(JsonFieldType.STRING).description("사용자 아이디"),
                                PayloadDocumentation.fieldWithPath("data.nickname").type(JsonFieldType.STRING).description("사용자 닉네임"),
                                PayloadDocumentation.fieldWithPath("data.phone").type(JsonFieldType.STRING).description("사용자 전화번호"),
                                PayloadDocumentation.fieldWithPath("data.email").type(JsonFieldType.STRING).description("사용자 이메일 주소"),
                                PayloadDocumentation.fieldWithPath("data.role").type(JsonFieldType.STRING).description("사용자 역할"),
                                PayloadDocumentation.fieldWithPath("errorCode").type(JsonFieldType.STRING).description("에러 코드 (있는 경우)").optional()
                        )
                ));
    }
}
  • 우리가 앞전에 짰던 코드이다. 거의 다 동일한데 두가지만 변경하면 이 틀을 유지해서 돌릴 수 있다.
    1. MockMvcRequestBuilders → RestDocumentationRequestBuilders
      • org.springframework.test.web.servlet.request.MockMvcRequestBuilders;org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
    2. MockMvcRestDocumentation → MockMvcRestDocumentationWrapper
      • org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;

실행 되는 순서

> Task :compileJava
> Task :processResources
> Task :classes
> Task :bootJarMainClassName
> Task :jar
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
> Task :check
> Task :openapi3
> Task :generateSwaggerUISample
> Task :copySwaggerUI
> Task :bootJar
> Task :assemble
> Task :build
  • Swagger에 관련된 Task들만 보겠다.
  1. openapi3: OpenAPI 3 사양에 따른 API 문서를 생성한다.
  2. generateSwaggerUISample: Swagger UI를 위한 리소스를 생성한다.
  3. copySwaggerUI: generateSwaggerUISample task로 생성된 Swagger UI 리소스를 프로젝트의 build/resources/main/static/docs 디렉토리로 복사한다.

Spring Rest Docs + Swagger 테스트 실행

  • 위 테스트 코드를 실행하면 아래와 같게 나온다.
    • openapi3.json → Spring Rest Docs의 문서 스니펫들을 기반으로 생성되는 OpenAPI 3.0 형식의 JSON 파일이다.
    • index.html → Swagger UI는 OpenAPI 혹은 Swagger 형식의 API 문서를 기반으로 사용자 웹 페이지 형태로 API 문서를 만든다.
      • index.html (Swagger UI 웹 페이지)는 앞서 생성된 openapi3.json 파일을 참조하여 만들어진 것이다.

Swaager UI

  • 그렇게 해서 생성 된 회원가입에 대한 API 명세서이다.
  • 우리가 익히 알고 있는 Swagger UI라 간단하게 넘어가겠다.

결론

후기

  • 이로서 내가 보기도 프론트 개발자가 보기도 기획자가 보기도 편한 API 명세서가 다 완성이 되었다.
  • 검증이 완료되어 만들어진 API 명세서라 신뢰성이 100% 이상이다!
  • 앞으로는 이제 데브옵스, 모니터링과 지금은 Common Module을 사용하는데 이걸 다른사람들도 편하게 사용하기 위해 오픈소스용 라이브러리를 하나 만들어볼 생각이다!
    • 벌써부터 기대가 된다ㅎㅎ 😄
profile
지나가는 개발자

0개의 댓글