Spring/Spring Boot

[Spring] restdocs + swagger ui 같이사용하기 (restdocs 문서 자동화)

민돌v 2023. 4. 14. 12:06
✨ api 문서화 정리 글
  1. Swagger
  2. Spring Rest Docs
  3. RestDocs + Swagger-UI 같이사용하기

 

 

오느른, 오늘은,, 오늘우리는,,,  Spring Restdocs 를 사용해 test 코드로 Ascciidoc 문서조각을 모으고,, 모아서 만든 adoc 문서를 또 편집하고.. html 로 변환하고 이 일련의 귀찮은 과정을 거쳐야하는 Spring Rest Docs 의 단점을 커버하기 위해

 

Swagger 와 restdocs 를 결합해보겠습니다 ! 👏🏻👏🏻👏🏻

 

사실 시작은 이러했습니다.

현재 사내에서는 Swagger 만을 사용하고 있기 때문에 프로덕션코드에 API 문서를 위한 코드가 존재하는게 지저분해서 현재 하고있는 토이프로젝트에는 RestDocs 를 적용해 보았습니다.

하지만, 아래처럼 깔금하게 원하는 형식대로 문서화가 진행되지 않았고 (제가 잘 몰라서 그런것일지돟ㅎㅎ), 또 docs 문서화를 위한 테스트코드 작성 까지는 좋은데 → 그 후에 다시 index.adoc 문서를 추가 작업해야하는게 너무 귀찮습니다 !!!!!

 

😂 그리고 막상 API 가 늘어나니까 보기도 굉장히 불편해 이쁜 Swagger UI 가 그리웠습니다..   

안예뻐.

 

 

그러다 지인이 좋은 인사이트를 주었다!!

 

 

바로 레츠고.


1.  📗 Swagger UI

  • Swagger 는 Swagger 와 Swagger UI 로 나뉘어집니다.
  • Swagger로 코드를 작성하면 OpenAPI 코드가 작성되고 이를 Swagger-UI 로 시각화 해주는식으로 동작합니다.
  • 즉, 꼭 Swagger를 사용하지 않아도 Swagger-UI 만을 사용할 수 있습니다.

 

그럼 이제, restdocs 로 작성된 문서를 Swagger-UI 가 읽을 수 있도록 변환해주는 설정을 먼저 해줘야겠죠?

 


2. ⚙️ restdocs-api-spec

  • https://github.com/ePages-de/restdocs-api-spec
  • ePages-de/restdocs-api-spec : Spring Rest Docs 의 테스트코드를 활용해 OpenAPI(Swagger 3.0) 의 결과를 만들어주는 오픈소스 라이브러리 입니다.
  • Spring Rest Docs 의 스펙을 따라가기위해 "MockMvc, WebTestClient, RestAssured" 모두 지원한다고 합니다.

 

사용방법도 간단하여 기존의 RestDocs로 작성된 문서도 거의 건들이지 않고도 수정할 수 있습니다.

단순하게 API 문서 생성부으 ㅣ구현체를 이 라이브러리에서 제공하는 구현체로 바꾸기만 해도 문서는 만들어집니다!

전체적인 구조는 아래 사진과 같습니다.

RestDocs 흐름 → RestDocs + Swagger UI 흐름


 

1) 설정하기

  • 기존의 adocs 문서를 만들기위한 의존성과 코드들은 전부 삭제했습니다.
  • 먼저 "rest-cods-api-spec" 플러그인을 추가해주고 + 밑에 MockMvc를 추가해줄 때 버전을 맞추기위해 버전만 변수로 추출했습니다.
buildscript {
   ext {
      restdocsApiSpecVersion = '0.17.1' // restdocsApiSpecVersion 버전 변수 설정
   }
}

plugins {
   id 'java'
   id 'org.springframework.boot' version '3.0.0'
   id 'io.spring.dependency-management' version '1.1.0'

   //restdocs-api-spec → restdocs extension  포함함
   id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"

   //swagger generator 플러그인 추가
   id 'org.hidetake.swagger.generator' version '2.18.2'

}


group = 'com.tht'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
targetCompatibility = '17'

configurations {
   compileOnly {
      extendsFrom annotationProcessor
   }
}

repositories {
   mavenCentral()
}

dependencies {

   //spring rest docs
   testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

    //restdocs-api-spec 의존성 추가
    testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
	
    ...

}


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

// Task 및 설정 추가
// GenerateSwaggerUI 태스크가, openapi3 task 를 의존하도록 설정
tasks.withType(GenerateSwaggerUI) {
   dependsOn 'openapi3'
   
   //기존 파일 삭제했다가, build 에 출력한 json 정적파일 복사 (안해도 됨 → local 확인용)
   	delete file('src/main/resources/static/docs/')
	copy {
		from "build/resources/main/static/docs"
		into "src/main/resources/static/docs/"

	}
}


//openAPI 작성 자동화를 위해 패키징 전에 openapi3 테스크 선실행을 유발
bootJar{
   dependsOn(':openapi3')
}

openapi3 {
   server = "http://tht-talk.store"
   title = "THT API 문서"
   description = "Spring REST Docs with SwaggerUI."
   version = "0.0.1"
   outputFileNamePrefix = 'open-api-3.0.1'
   format = 'json'

   // /static/docs/open-api-3.0.1.json 생성 → jar 파일만 배포할 예정이기에 build 에 출력
	outputDirectory = 'build/resources/main/static/docs'
}

 

openapi3 설정

openapi3 로 Open API 3 스펙을 만들때 필요한 부가정보들 입니다.
  • setServer(...) : 서버 주소를 설정합니다. API 요청 보내기 기능에서 이 주소가 사용됩니다.
  • title : API 문서의 제목
  • description : API 문서의 설명
  • version : API 문서의 버전
  • format : API 문서 출력 포멧(default : JSON)
  • outputDirectory : format 형식으로 변환할 파일을 저장할 디렉토리 경로를 설정할 수 있습니다.

 

이렇게하면 설정은 끝입니다. 이제 테스트코드를 작성해 봅시다!

 

 

2) 테스트코드 작성

변경을 최소화하고 싶으시다면 패키지 설정에 딱 1줄만 바꾸면 됩니다 !

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;

import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document;

 

구현체를 이렇게만 바꿔도 동작은 하지만 조금 더 깔끔하게 작성하기 위해서, 나머지 테스트 코드 작성방법도 정리해두겠습니다.

 

 

공통부분 추상화

  • 모든 Controller 테스트에 들어가는 부분을 추상화한 클래스 입니다.
  • restdocs 를 사용할 때와 다르게 @AutoConfigureMockMvc 와 @AutoConfigureRestDocs 두 어노테이션 꼭 작성해야한다고 합니다.

 

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
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.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;

@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ExtendWith({RestDocumentationExtension.class})
public abstract class ControllerTestConfig {

    @Autowired
    protected WebApplicationContext ctx;

    @Autowired
    protected ObjectMapper objectMapper;

    protected MockMvc mockMvc;

    @BeforeEach
    void setUp(final RestDocumentationContextProvider restDocumentation) {
        mockMvc = MockMvcBuilders.webAppContextSetup(ctx)
            .apply(documentationConfiguration(restDocumentation))
            .addFilters(new CharacterEncodingFilter("UTF-8", true))
            .alwaysDo(print())
            .build();
    }

}

 

Controller Code

  • Get 방식이면 응답값이 List 형식이기 때문에 [].idx 이런식으로 접근을했습니다.
  • 단일 객체라면 idx 바로 사용하면 되고. 
  • List 를 멤버변수를 가지고 있다면 key.idx 이런식으로 접근하면 될겁니다
import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
import static org.mockito.Mockito.when;
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 com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.epages.restdocs.apispec.Schema;
import com.tht.api.app.facade.interest.InterestFacade;
import com.tht.api.app.facade.interest.response.InterestResponse;
import java.util.List;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

@WebMvcTest(InterestController.class)
class InterestControllerTest extends ControllerTestConfig {

    private static final String DEFAULT_URL = "/interests";

    @MockBean
    InterestFacade interestFacade;

    @Test
    @DisplayName("관심사 목록 리스트 조회")
    void getInterestAll() throws Exception {

        //given
        when(interestFacade.getInterestList()).thenReturn(
            List.of(new InterestResponse(1, "이상형 명칭", "emoji code"))
        );

        //then
        ResultActions resultActions = mockMvc.perform(
            RestDocumentationRequestBuilders.get(DEFAULT_URL)
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
        ).andDo(
            MockMvcRestDocumentationWrapper.document("interest-docs",
                preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint()),
                resource(
                    ResourceSnippetParameters.builder()
                        .description("관심사 목록 리스트 조회")
                        .requestFields()
                        .responseFields(
                            fieldWithPath("[].idx").description("idx"),
                            fieldWithPath("[].name").description("이상형 명칭"),
                            fieldWithPath("[].emojiCode").description("이모티콘 코드")
                        )
                        .build()
                )
            )
        );

        resultActions.andExpect(MockMvcResultMatchers.status().isOk());
    }

}
public record InterestResponse (Integer idx, String name, String emojiCode){}

응답 형식

 

 

이렇게 작성하면 gradle 에서 설정해든 output 디렉토리에 json 형식의 파일이 생성됩니다 !

json 파일을 열어서 확인해보면 Swagger UI 에서 인식하는 기본 샘플 형식과 같은 형식으로 데이터가 추출되었음을 확인할 수 있습니다.

 

이제 Swagger UI를 적용해봅니다.

 


 

3. 👍 Swagger-UI 적용하기

많은 분들이 Swagger 서버를 도커로 따로 띄어서 사용하시던데, 저는 MSA 환경이 아니기 때문에 Spring 에 의존성을 추가하여 바로 사용해보도록 하겠습니다.

 

사실상 의존성 하나만 추가해도 Swagger UI 를 사용할 수 있습니다.

이게 Spring boot 3.0 이라 그런지 java 17 이라 그런지.. open-api 가장 최신버전을 사용해서 그런지 모르겠지만 하단의 의존성을 추가해야지만 Swagger 화면이 정상적으로 나왔습니다.

// Swagger ui - https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui
// implementation 'org.springdoc:springdoc-openapi-ui:1.7.0'  → 이거 안됨

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

 

의존성을 추가하고 다시 빌드해서 👉 "http://localhost:8080/docs/swagger-ui/index.html" 경로에 들어가면 Swagger 기본 디폴트 페이지가 뜸을 확인할 수 있습니다.

 

하지만, 기본 페이지가 아닌 저희가 만들어준 Json 파일을 바라보도록 swagger default 를 수정해줘야겠져

 

 

📌 Swagger default Json 경로 수정

  • application.yaml 파일에서 간단한 설정을 할 수 있습니다.
  • swagger-ui.url : swagger-ui가 읽은 파일 경로
  • swagger-ui.path : swagger-ui 띄울 url 변경 → 이러면 "http://localhost:8080/docs/docs/swagger" 를 통해 문서를 불러올 수 있습니다.
springdoc:
    default-consumes-media-type: application/json;charset=UTF-8
    default-produces-media-type: application/json;charset=UTF-8
    swagger-ui:
        url: /docs/open-api-3.0.1.json
        path: /docs/swagger

 

이렇게 설정을 하면 짠! 하고 설정한 restdocs 로 만든 문서화가 만들어졌습니다.

 

 

 

 

📌 Swagger UI 이쁘게 적용하기

  • 다좋은데 기본적으로 설정을 하지 않으면 기본 Swagger 만 사용했을 때처럼 이쁘지가 않습니다.
  • 위의 "이상형 목록", "관심사 목록", "유저" 도 저렇게 한글이 아닌 영어로 나올텐데
  • 이 주제와 아래의 Schema 를 Response 형식 이름을 이쁘게 바꿔보겠습니다  

 

 

간단하게 Resource Build 설정에서 값을 변경할 수 있습니다.

//then
ResultActions resultActions = mockMvc.perform(
    RestDocumentationRequestBuilders.get(DEFAULT_URL)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
).andDo(
    MockMvcRestDocumentationWrapper.document("ideal-types-docs",
        preprocessRequest(prettyPrint()),
        preprocessResponse(prettyPrint()),
        resource(
            ResourceSnippetParameters.builder()
                .tag("이상형 목록")
                .description("이상형 목록 리스트 조회")
                .requestFields()
                .responseFields(
                        fieldWithPath("[].idx").description("idx"),
                        fieldWithPath("[].name").description("이상형 명칭"),
                        fieldWithPath("[].emojiCode").description("이모티콘 코드")
                )
                .responseSchema(Schema.schema("IdealTypeResponse"))
                .build()
        )
    )
);
  • ResourceSnippetParameters.builder()
    직접 작성할 API 정보를 추가할 때 사용됩니다.
  • tag(String)
    위의 이미지에서 “테스트"에 해당하는 부분으로 태그를 통해 여러API를 묶을 수 있습니다.
  • summary(String)
    API의 요청 URL 옆에 들어가는 제목입니다.
  • description(String)
    API 세부 정보로 들어갔을 때 존재하는 설명입니다.
  • requestSchema(Schema)
    해당 API를 호출할때 사용한 본문과 매핑되는 객체의 이름입니다.
  • responseSchema(Schema)
    해당 API의 반환 값의 본문과 매핑되는 객체의 이름입니다.

 

 

쨘! 잘나오네요

 

 

 

이렇게해서 Restdocs로 테스트 코드만 작성하면 추가적인 문서 편집없이 

Swagger UI 로 표출되도록 설정을 끝마쳤습니다.

 

 

 

너무 만족스러운 작업이네요

그럼 20000 ~

 

 


*참고