REST Docs와 Swagger ui

기존 Swagger 사용의 단점

  • 비즈니스 로직에 문서화 코드가 들어가야한다.
  • 검증과정을 거치지 않았기 때문에 불안정하다.
  • MSA와 같이 분산된 서비스에서  각각의 문서가 전부 다른 주소를 갖기 때문에 일일이 찾아봐야한다.

 

해결 방안

  1. REST Docs를 이용해서 테스트를 거친 검증된 문서를 생성
  2. open api spec 추출
  3. Swagger ui로 api spec을 보내 API 문서 통합

 

적용

REST Docs

 

1. Gradle 의존성 & 빌드 스크립트 추가

//(1)
plugins {
	id "org.asciidoctor.jvm.convert" version "3.3.2"
}

//(2)
configurations {
	asciidoctorExt 
}

//(3)
dependencies {
	asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' 
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' 
}

//(4)
ext { 
	snippetsDir = file('build/generated-snippets')
}

//(5)
test { 
	outputs.dir snippetsDir
}

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

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

//(8)
task copyDocument(type: Copy){	
    dependsOn asciidoctor		
    from file("build/docs/asciidoc")
    into file("src/main/resources/static/docs")
}

//(9)
bootJar {
    dependsOn copyDocument
    from ("${asciidoctor.outputDir}/html5") {
        into 'static/docs'
    }
}

 

2. 테스트코드 작성

 

package com.example.orderservice;

import com.example.orderservice.payment.vo.complete.RequestPayment;
import com.example.orderservice.payment.vo.refund.RequestRefund;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
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.restdocs.payload.JsonFieldType;
import org.springframework.test.web.servlet.MockMvc;


import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.payload.PayloadDocumentation.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
class OrderServiceApplicationTests {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;


    @Test
    @DisplayName("사전 검증")
    void prepareVerificationTest() throws Exception{

        //given
        int amount = 9900;

        //when
        mockMvc.perform(post("/payment/prepare/{amount}",amount)
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                //then
                .andExpect(status().isOk())
                // rest docs 문서화
                .andDo(document("prepareVerification",
                        responseFields(
                                fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답 코드"),
                                fieldWithPath("message").type(JsonFieldType.STRING).description("응답 메세지").optional(),
                                fieldWithPath("response").type(JsonFieldType.OBJECT).description("응답 데이터"),
                                fieldWithPath("response.merchant_uid").type(JsonFieldType.STRING).description("결제 아이디"),
                                fieldWithPath("response.amount").type(JsonFieldType.NUMBER).description("금액"),
                                fieldWithPath("response.token").type(JsonFieldType.STRING).description("토큰")
                        )
                ));
    }

    @Test
    @DisplayName("사후 검증")
    void completeVerificationTest() throws Exception{
        //given
        RequestPayment request = new RequestPayment(
                "imp_442601173622",
                "imp464605542023-04-05T16:43:08.675766",
                "5b5825a581c67c7c345811f74b2b27be216b91a0",
                "ABC@Email.com",
                1,
                9900
                );

        //when
        mockMvc.perform(post("/payment/completion")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
        //then
                .andExpect(status().isOk())
                .andDo(document("completionVerification",
                        requestFields(
                                fieldWithPath("imp_uid").type(JsonFieldType.STRING).description("포트원 고유번호"),
                                fieldWithPath("merchant_uid").type(JsonFieldType.STRING).description("결제 고유번호"),
                                fieldWithPath("token").type(JsonFieldType.STRING).description("엑세스 토큰"),
                                fieldWithPath("userEmail").type(JsonFieldType.STRING).description("유저 이메일"),
                                fieldWithPath("itemId").type(JsonFieldType.NUMBER).description("아이템 아이디"),
                                fieldWithPath("amount").type(JsonFieldType.NUMBER).description("총액")
                        ),
                        responseFields(
                                fieldWithPath("status").type(JsonFieldType.STRING).description("결제 상태"),
                                fieldWithPath("message").type(JsonFieldType.STRING).description("메세지").optional()
                        )
                ));
    }

    @Test
    @DisplayName("환불")
    void refund() throws Exception{
        //given
        RequestRefund request = new RequestRefund(
                "ABC@Email.com",
                "imp464605542023-04-05T16:43:08.675766",
                9900,
                "단순 변심"
        );
        //when
        mockMvc.perform(post("/payment/cancel")
                        .content(objectMapper.writeValueAsString(request))
                        .contentType(MediaType.APPLICATION_JSON)
                        .accept(MediaType.APPLICATION_JSON))
                //then
                .andExpect(status().isBadRequest())
                .andDo(document("refund",
                        requestFields(
                                fieldWithPath("userEmail").type(JsonFieldType.STRING).description("유저 이메일"),
                                fieldWithPath("merchant_uid").type(JsonFieldType.STRING).description("결제 고유번호"),
                                fieldWithPath("cancel_request_amount").type(JsonFieldType.NUMBER).description("환불 금액"),
                                fieldWithPath("reason").type(JsonFieldType.STRING).description("환불 사유")
                        ),
                        responseFields(
                                fieldWithPath("message").type(JsonFieldType.STRING).description("환불 결과 메세지")
                        )
                ));
    }
}

 

3. 문서화 하기

테스트에 성공하면 아래와 같이 build/generated-snippets 폴더 안에 테스트 코드에서 endDo에 document이름으로 지정한 폴더가 들어있고 그 안에 adoc 파일들이 생성되어 있다.

위 사진처럼 src/docs/asciidoc 폴더 생성 후  adoc 파일을 생성한 다음 문서구조를 작성해준다.

[[api-completionVerification]]
== 사후 검증 서비스(Completion Verification API)

====== 요청 형식
include::{snippets}/completionVerification/http-request.adoc[]
====== Request Body
include::{snippets}/completionVerification/request-body.adoc[]
====== 응답 형식
include::{snippets}/completionVerification/http-response.adoc[]
include::{snippets}/completionVerification/response-fields.adoc[]
====== Response Body
include::{snippets}/completionVerification/response-body.adoc[]
====== Try with curl
include::{snippets}/completionVerification/curl-request.adoc[]

작성이 끝난 후 빌드하면 html 파일이 생성된다.

./gradlew clean bootJar

 

 

open api spec 추출

 

1. Gradle 의존성 & 빌드 스크립트 추가

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

dependencies {
	testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.16.2'
}

openapi3 {
    server = 'https://localhost:8080'
    title = 'Octo Dream'
    description = 'OctoDream API Specification'
    version = '0.0.1'
    format = 'yml'
    outputFileNamePrefix = 'payment-service'
    outputDirectory = 'src/main/resources/static/docs'
}

 

2. 테스트 코드에서 import 수정 

//import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
//위 코드를 아래 코드로 수정
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;

 

3. open api 추출

./gradlew openapi3

빌드 성공 시 yml 파일이 생성된다.

 

 

 

Swagger ui로 문서 통합

 

https://swagger.io/docs/open-source-tools/swagger-ui/usage/installation/

 

Swagger Documentation

Installation Distribution channels NPM Registry We publish three modules to npm: swagger-ui, swagger-ui-dist and swagger-ui-react. swagger-ui is meant for consumption by JavaScript web projects that include module bundlers, such as Webpack, Browserify, and

swagger.io

swagger ui로 문서를 통합하는 방법은 공식문서에 나와있는 것 처럼 도커를 사용하는 방법과 html파일을 사용하는 방법이 있다.

둘 중 선택해서 사용하면 된다.

 

 

문서 화면

 

생성된 api spec swagger ui서버에 올리는 방법

 docker pull swaggerapi/swagger-ui
sudo docker run -d -p 80:8080 \
--name swagger \
-e URLS_PRIMARY_NAME=user-service \
-e URLS="[{ url: 'docs/user-service.yml', name: 'user-service' }, { url: 'docs/payment-service.yml', name: 'payment-service' }]" \
-v /home/ubuntu/docs:/usr/share/nginx/html/docs/ \
swaggerapi/swagger-ui

 

 

참고 사이트

https://velog.io/@suhongkim98/spring-Rest-Docs-Swagger-UI%EB%A1%9C-MSA-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C-API-%EB%AC%B8%EC%84%9C-%ED%86%B5%ED%95%A9-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0

 

spring Rest Docs + Swagger UI로 MSA 환경에서 API 문서 통합 관리하기

마이크로서비스 아키텍처에서 API 문서 통합 관리하기

velog.io

'개발 > Spring' 카테고리의 다른 글

[JPA] Query Method & JPQL  (0) 2023.04.23
[JSP] Cookie와 Session  (0) 2023.04.18
Java MVC 패턴을 이용한 간단한 미니 쿠팡 만들기  (0) 2023.04.04
DAO, DTO, VO  (0) 2023.03.16
Spring WebFlux로 Chatting service 만들기  (0) 2023.02.27