[Java] Spring Boot REST Docs + Swagger UI 연동

Spring REST Docs와 Swagger UI를 연동하여 사용하는 방법을 예제 프로젝트와 함께 소개합니다.

공공데이터 특일 API를 동시에 사용할 프로그램들이 늘어났다. 그런데 이건 이전 글에서 소개한 것처럼 일반적이지 않게 설계되어 있어 로직에 대해 예외처리를 자주 해야하는 상황이 있다. 또한, 동일한 API를 여러 프로그램에서 사용하니 중복 코드의 문제와 API Token의 Limit 문제도 존재한다.

중복 된 코드를 최소화하고 JSON 형태로 뿌려주어 일반적인 형태로 제공할 수 있도록 기능을 제공하도록 API를 재설계하였다. 당연히 API를 만들었으니 Document도 필요한 상황이었는데, 기존에는 Swagger UI를 통해서 만드는 경우도 있는데 이 경우는 자동으로 만들어주는 것이 아니라 어노테이션을 통해서 수동으로 정의해줘야한다.

이걸 좀 자동화할 방법이 없나 해서 Spring Boot REST Docs를 알아보았다. Spring Boot REST Docs는 Test 코드를 통해서 검증되어야만 문서가 생성이 된다는 점이 장점이라고 할 수 있다. Test를 통과해야 문서가 생성되니 귀찮더라도 Test Coverage를 높이면서 문서의 최신화까지 꾀할 수 있는 것이다.

문서의 랜더링 방식에는 여러가지가 있지만 공통적으로 'Try it out'을 할 수 없다는 점이다. 그런데, Swagger UI는 가능하기에 장점인 부분도 분명히 존재한다. 그래서 Spring REST Docs와 Swagger UI의 장점을 결합할 방법이 있을까? 했는데 정답은 '있다' 이다.

그래서 이번에는 Spring REST Docs와 Swagger UI를 연동하는 예제를 소개해보도록 하겠다.


Environment

  • Java 17 (MS JDK 17.0.9)
  • Spring Boot 3.3.2
  • Gradle 8.5 (Groovy)
  • Lombok
  • org.asciidoctor.jvm.convert 4.0.4
  • com.epages.restdocs-api-spec 0.19.4
  • org.hidetake.swagger.generator 2.19.2
  • com.epages:restdocs-api-spec-mockmvc 0.17.1

Concepts

이번 글에서는 예제 형태의 글이므로 예제 API인 '사용자 관리' API를 하나 만들어 소개하겠습니다.

  1. 사용자 목록 조회 (GET /api/users)
  2. 특정 사용자 조회 (GET /api/users/{id})
  3. 새 사용자 생성 (POST /api/users)
  4. 사용자 정보 수정 (PUT /api/users/{id})
  5. 사용자 삭제 (DELETE /api/users/{id})

Step

Gradle Plugin, Dependency 추가

먼저, 작업에 필요한 플러그인과 의존을 추가한다.

// Gradle Plugin
id 'org.asciidoctor.jvm.convert' version '3.3.2'
id 'com.epages.restdocs-api-spec' version '0.19.4'
id 'org.hidetake.swagger.generator' version '2.19.2'
// Dependency
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.17.1'

본인은 위의 의존만 추가해도 아무 문제가 없었다.

build.gradle Plugin, Dependency 추가 완료

추가하면 위 사진과 같이 구성된다.

Gradle Step 추가

그 다음, Build 과정에서 자동으로 Open API 명세가 JSON 형태 파일로 Output 되도록 정보와 Output 위치, 파일 이름 등 여러 가지를 결정해야한다. 나는 아래와 같이 설정했다.

openapi3 {
    servers = [
            // 2개 이상 설정하고자 하는 경우, 추가 가능
            { url = "http://localhost:8080"; description = "Local server" }
    ]
    title = "Spring Restdocs with SwaggerUI Sample"
    description = "This is made with Spring REST Docs with SwaggerUI."
    version = "1.0.0"
    format = 'json'
    outputDirectory = 'build/resources/main/static/docs'
}

bootJar {
    dependsOn(':test')
    dependsOn(':openapi3')
}

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

특히, openapi3에 있는 title, servers, description 등은 상황에 맞게 고치시면 된다. servers 부분은 Array 형태임에도 하나만 기재되어 있는데 사용하는 상황에 맞게 추가해서 사용하실 수 있도록 설정하였다.

테스트 작성

위에서도 소개했다 싶이 Spring REST Docs는 테스트를 통과해야만 문서가 생성된다. 그래서 MockMvc를 이용해서 테스트를 진행한다. 예제 소스에서는 Controller는 하나지만, Endpoint 요청별로 여러 케이스를 만들어서 테스트하여서 각 Endpoint 요청마다 테스트 클래스를 만들었다.

@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@ActiveProfiles("test")
class UserApiControllerUpdateUserTests {
    @Autowired
    private MockMvc mockMvc;

    @MockitoSpyBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void updateUser_Success() throws Exception {
        assertTrue(Mockito.mockingDetails(userService).isSpy());

        final UserUpdateRequestDto userUpdateRequestDto = UserUpdateRequestDto.builder()
                .name("test")
                .email("[email protected]")
                .phone("010-1234-5678")
                .build();

        doReturn(ResponseEntity.noContent().build())
                .when(userService).updateUser(anyLong(), any(UserUpdateRequestDto.class));

        mockMvc.perform(put("/api/users/{id}", 1)
                        .content(objectMapper.writeValueAsString(userUpdateRequestDto))
                        .contentType("application/json")
                )
                .andExpect(status().isNoContent())
                

UserApiControllerUpdateUserTests.java의 일부

위의 코드는 UserApiControllerUpdateUserTests.java의 일부이다. 테스트를 위해서 MockMvc 객체를 만들고 로직을 수행할 UserService를 SpyBean을 통해서 가짜 객체를 넣어준다. 그래야 실제 DB에 넣지 않고 Controller가 제대로 동작하는지 테스트할 수 있다.

특히, 어노테이션에 주목해야하는데, '@MockitoSpyBean'의 경우 Spring Boot 3.4.0 부터 사용되는 것으로 미만의 버전을 사용한다면 @MockBean을 사용하기 바란다.

assertTrue(Mockito.mockingDetails(userService).isSpy()); 이 코드를 통해 Service가 Spy를 통해 가짜 객체로 주입됐는지 확인한다. 그 다음, 응답을 Mocking하고, MockMvc를 통해 응답에 대한 테스트를 진행한다.

API Endpoint 명세

위 코드에서 테스트에서 통과한 경우, 예제에 사용했던 Request Parameter나 Path Variables, Body 에 들어간 항목들에 대해서 명세하고 응답에 있는 내용도 명세해야한다. 아래 코드를 참고하면 된다.

                .andDo(document("update-user",
                        resource(
                                ResourceSnippetParameters.builder()
                                        .summary("User의 정보를 업데이트하는 Endpoint")
                                        .description("특정 User의 Id를 Path로 지정하고, Body에 업데이트할 정보를 담아서 호출합니다.")
                                        .requestSchema(Schema.schema("UserUpdateRequest"))
                                        .responseSchema(Schema.schema("UserUpdateResponse"))
                                        .pathParameters(
                                                parameterWithName("id").description("업데이트 할 User의 id").attributes(key("type").value("number"))
                                        )
                                        .requestSchema(Schema.schema("UserUpdateRequest"))
                                        .requestFields(
                                                fieldWithPath("name").type(JsonFieldType.STRING).description("업데이트할 유저의 이름. null로 설정 시, 업데이트 하지 않음.").optional(),
                                                fieldWithPath("email").type(JsonFieldType.STRING).description("업데이트할 유저의 이메일. null로 설정 시, 업데이트 하지 않음.").optional(),
                                                fieldWithPath("phone").type(JsonFieldType.STRING).description("업데이트할 유저의 전화번호. null로 설정 시, 업데이트 하지 않음. Blank로 요청시, 기존 정보를 삭제.").optional()
                                        )
                                        .build()
                        )

UserApiControllerUpdateUserTests.java의 일부

Builder는 아래 정보를 참고하여 채우면 된다.

  • Summary: Endpoint 옆에 표시될 간단한 설명
  • Description: 자세히 보기를 눌렀을 때 나오는 것으로 상세한 설명을 기제하면 된다.
  • Schema: Example 요청의 이름을 정한다. 지정하지 않아도 되지만 임의의 문자로 지정되어 문서를 보는 사람이 곤란할 수 있다.
  • pathParameters: Path Variable가 있을 경우 정의하면 된다.
  • requestSchema: Body 안에 들어가야할 내용이 있으면 정의하면 된다.
  • queryParameters: Request Parameter에 들어가야할 내용이 있으면 정의하면 된다.
  • responseFields: Response에 들어가는 내용을 정의하면 된다.

Result

이렇게 작성한 코드의 결과를 확인해보자. 실제로 프로젝트를 실행해서 확인해보면 아래와 같이 Swagger UI가 제대로 표출되는 것을 볼 수 있다.

Overview를 보면, 설계했던 API Endpoint 들이 제대로 표출되는 것을 볼 수 있다.

특정 요청을 보면, 작성했던 Description과 Parameters 작성 요령이 보이며 유형별 Request Example가 표출된다.

PUT을 보면 다양한 Response Code 시나리오가 표출됨을 확인할 수 있다.

Source

전체 소스는 Github 에서 확인할 수 있다.

Reference

구현에 아래 글들을 참고 했다.