개발/기능 개발

[Spring] Rest Docs와 Swagger UI

뽀글뽀글 개발자 2023. 4. 11. 23:39

기존 Swagger 사용의 단점

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

 

해결 방안

  1. Rest Docs를 통해 테스트 코드에서 문서화 코드를 작성하여, 프로덕션 코드와 분리하고 검증된 문서 생성
  2. Rest Docs에 의해 생성된 open api spec 추출하여 Swagger UI로 api spec을 보내 마이크로서비스들의 API 문서 통합

 

적용

Rest Docs 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'
    }
}

 

 

테스트코드 작성

@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("환불 결과 메세지")
                        )
                ));
    }
}

 

문서화 하기

테스트에 성공하면 아래와 같이 build/generated-snippets 폴더에 테스트 코드의 .andDo에 지정한 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 추출

 

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'
}

 

테스트 코드에서 import 수정 

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

 

open api 추출

./gradlew openapi3

 

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

 

 

 

Swagger ui로 문서 통합

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

 

문서 화면

 

 

생성된 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