MSA: Swagger UI로 API 문서 통합 프로세스 (3) Gradle Task from ePages Plugin

Letsdev·2023년 5월 28일
3
post-custom-banner

Index

1: 독립형 Swagger UI 서비스
2: 도커 컴포즈 파일
3: Rest Docs를 Open API로 정적 배포(Gradle Task)
4: Web Config(CORS) 및 Security Config

스웨거 유아이가 다른 서버 애플리케이션들로부터 오픈 에이피아이 문서를 받아서 제공하고 있는 그림. 이전 글들에서도 같은 그림을 사용했다.


Plugin

플러그인은 이렇게 두 개를 추가한다.

  • Rest Docs 문서를 위한 Asciidoctor
  • Open API 양식으로 변환해 줄 epages의 restdocs-api-spec

의존성도 넣어 놨다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.0'
    
    // Added:
    id 'org.asciidoctor.jvm.convert' version '3.3.2'
    id 'com.epages.restdocs-api-spec' version '0.17.1'
}

dependencies {
	// ...

    // RestDoc to Open API
    implementation "org.springdoc:springdoc-openapi-ui:1.6.14"
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.17.1'
}

멀티 모듈인 경우 예시

멀티 모듈인 경우 apply plugin: 'org.asciidoctor.jvm.convert' 등도 적용해 준다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.0'
    id 'io.spring.dependency-management' version '1.1.0'
    
    // Added:
    id 'org.asciidoctor.jvm.convert' version '3.3.2'
    id 'com.epages.restdocs-api-spec' version '0.17.1'
}

// if multi-module project (example)
allprojects {
	// ...
    
    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'org.asciidoctor.jvm.convert'
    apply plugin: 'com.epages.restdocs-api-spec'
    
    // ...
    
    dependencies {
    	// ...
        implementation 'com.google.code.gson:gson:2.10.1'
        
        // RestDoc
        implementation "org.springdoc:springdoc-openapi-ui:1.6.14"
        testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
        testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.17.1'
	}
}

Task

epages에서 제공한 위 변환 플러그인을 추가했다면, openapi, openapi3, postman 등 태스크를 사용할 수 있다.

그중 우리는 openapi3 태스크를 사용할 것이다.

이처럼 build.gradle에 태스크를 추가해 주고,
테스트 후 알아서 수행되도록 테스트 작업에 추가해 준다.

멀티모듈인 경우 아래 예시는 allprojects { ... } 안에 작성하면 된다.
(루트프로젝트를 사용하지 않으면 subprojects { ... } 안에 작성해도 된다.)

tasks.named('test') {
    useJUnitPlatform()
    finalizedBy 'openapi3' // added
}

// rest docs open api
openapi3 {
    println("project.rootProject.rootDir: $project.rootProject.rootDir")
    println("project.name: ${project.name}")

    server = 'https://localhost:8080'
    title = 'My API'
    description = 'My API description'
    // tagDescriptionsPropertiesFile = "${project.rootProject.projectDir}/docs/tag-descriptions.yml"
    version = '0.1.0'
    format = 'json'
    outputDirectory = 'src/main/resources/static/docs'
    outputFileNamePrefix = "openapi3.${project.name}"

    project.mkdir "${project.projectDir}/${outputDirectory}"
}

Test Example

이런 API를 테스트해 보겠다.

@RestController
public final class SampleApi {
    public record SampleRequestDto(@NotBlank String name, @NotNull @Min(0) Integer age) {
        public SampleRequestDto { name = name.strip(); }
    }
    @Builder
    public record SampleResponseDto(Boolean success) {}

    @PostMapping("/")
    public SampleResponseDto sample(@RequestBody @Valid SampleRequestDto body) {
        return SampleResponseDto.builder()
                .success(true)
                .build();
    }
}

테스트 코드와 문서화 과정이다.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureMockMvc   // MockMvcBuilders.webAppContextSetup(webApplicationContext) ... .build()
@AutoConfigureRestDocs  // .apply(documentationConfiguration(restDocumentation))
@ExtendWith(SpringExtension.class)
class SampleApiTest {
    @Autowired private MockMvc mockMvc;
    @Autowired private Gson gson; // object mapper를 사용해도 됨.

    @Test
    public void test() throws Exception {
        SampleRequestDto dto = new SampleRequestDto("홍길동", 17);

        ResultActions perform = this.mockMvc.perform(post("/")
                                .content(gson.toJson(dto)) // {"name": "홍길동", "age": 17}
                                .contentType(MediaType.APPLICATION_JSON)
                                .accept(MediaType.APPLICATION_JSON)
//                        .with(csrf())
                )
                .andExpect(status().is2xxSuccessful());

        perform.andDo(print())
                .andDo(
                        document("my-sample-api-identifier",
                                // pathParameters(parameterWithName("").description(""), ... ),
                                requestFields(
                                        fieldWithPath("name").description("이름"),
                                        fieldWithPath("age").description("나이")
                                ),
                                responseFields(
                                        fieldWithPath("success").description("성공")
                                )
                        )
                )
                .andDo(
                        document("sample",
                                preprocessRequest(prettyPrint()),
                                preprocessResponse(prettyPrint()),
                                resource(
                                        ResourceSnippetParameters.builder()
                                                .summary("API 설명 요약입니다.")
                                                .description("이것이 바로 API 설명입니다.")
                                                // .pathParameters(parameterWithName("").description(""), ... ),
                                                .requestFields(
                                                        fieldWithPath("name").description("이름"),
                                                        fieldWithPath("age").description("나이")
                                                )
                                                .responseFields(
                                                        fieldWithPath("success").description("성공")
                                                )
                                                .build()
                                )
                        )
                );
    }
}

임포트한 것들이 헷갈리면 참고하면 된다.
(일부는 epages가 제공하는 것으로 대체하여 사용해도 됨.)

import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.example.demo.v1.api.SampleApi.SampleRequestDto;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessRequest;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.preprocessResponse;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
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.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

이렇게 했을 때 src/main/resourcesstatic/docs/openapi3.{프로젝트이름}.json 이런 파일이 생겨나면 된다. 열어 보면 { "openapi" : "3.0.1", ... 이렇게 시작하고 있을 것이다. 우리가 실행해 둔 Swagger UI 컨테이너가 이 Open API 문서를 읽기 때문에, 각 서버 애플리케이션은 여기까지만 작업을 수행해 주면 된다.

대충 이런 과정이다.

Test + Create Rest Docs
Ascii Doctor: mk build/generated-snippets/*/~.adoc files
ePages: mk opanapi3.{yourProjectName}.json

이렇게 하고 나면, Swagger UI 화면이 이 서버 애플리케이션 API에 접근할 수 있도록 CORS 설정이 필요할 수 있다.
(이 설정을 생략해도 되는지 모르겠다면, 생략하면 안 되는 사람일 것이다.)

Web Config(CORS) 및 Security Config >
< 도커 컴포즈 파일


profile
아 성장판 쑤셔 (블로그 이전) https://letsdev.hashnode.dev
post-custom-banner

0개의 댓글