프로젝트/Ku:room

Spring REST Docs와 Swagger-UI 결합하기

개발하는 민우 2025. 2. 19. 23:30

https://helloworld.kurly.com/blog/spring-rest-docs-guide/

 

내가 만든 API를 널리 알리기 - Spring REST Docs 가이드편

'추석맞이 선물하기 재개발'에 차출되어 API 문서화를 위해 도입한 Spring REST Docs 를 소개합니다.

helloworld.kurly.com

 

들어가기 앞서

위 글을 보고, 프로젝트에 RestDocs와 Swagger-UI를 적용해본 이유와 과정을 작성해보고자 합니다.

Swagger의 단점

기존에 사용하던 스웨거는 정말 편리하지만, 운영코드에 침투적이라는 큰 단점이 있다.

RestDocs를 적용함으로써, 컨트롤러 단에서 테스트 코드를 작성해야 하므로, 'Spring RestDocs'를 적용해보았습니다.

 

@GetMapping
    @ApiOperation(value = "현재 사용자 조회",
            notes = "현재 사용자의 정보를 가져옵니다. 헤더(Bearer)에 사용자 토큰 주입을 필요로 합니다.")
    @ApiResponses({
            @ApiResponse(responseCode = "200", description = "사용자를 정상적으로 조회한 경우"),
            @ApiResponse(responseCode = "404", description = "해당 사용자를 찾지 못한 경우")
    })
    public ResponseEntity<UserResponse> getUser() {
        return ResponseEntity.ok(UserResponse.toUserResponse(userService.getUser()));
    }

- 스웨거 API 문서에서 정보를 풍부하게 제공하려고 하면, 운영코드에 스웨거 애노테이션이 침투하기 시작하며 생각보다 많은 코드를 작성하게 됩니다.

 

Spring REST Docs 는 테스트를 반드시 작성해야 한다.

 

 

"Spring REST Docs"는 API 문서에 포함(include)되는 "스니펫(snippets)"을 생성하기 위해 테스트 코드를 작성해야 합니다.

테스트 코드를 작성하지 않으면 스니펫을 얻을 수 없고, 스니펫을 얻지 못하면 API 문서에 포함시킬 수 없습니다. 고로, API 문서를 제공하기 위해서 반드시 테스트 코드를 작성해야 합니다.

 

따라서 제 프로젝트에서는, 최소 컨트롤러 단에서 테스트 코드를 보장하기 위해 "Spring REST Docs"를 사용하였습니다.

 

Spring REST Docs, 보기보다 더 힘들다.

 

- 작성된 테스트 실행 결과 아스키닥(Asciidoc) 스니펫을 수집해서 "아스키닥터(asciidoctor)" 플러그인을 이용해서 HTML 문서로 렌더링 됩니다. 여기서 마크다운(markdown)이 제공하지 못하는 기능을 제공합니다. 스니펫을 비롯해서 개발자가 임의적으로 분리한 아스키닥 원본파일을 원하는 형태로 조합하게 됩니다. 

 

- 이런식으로 모든 필드에 대해서 아스키닥 스니펫을 개발자가 수집하여, API 문서를 수동으로 제작해야 합니다.

 

Spring REST Docs + Swagger-UI 적용해보자.

이러한 수동 문서화 작업을 없애기 위해, 자동으로 Spring Rest Docs가 적용된 아스키닥 스니펫을 자동으로 Swagger-UI로 변환하도록 프로젝트를 리팩토링 해보았습니다.

 

[build.gradle]

 

// 1. Import 추가
import com.sun.security.ntlm.Server
import org.hidetake.gradle.swagger.generator.GenerateSwaggerUI
import org.springframework.boot.gradle.tasks.bundling.BootJar

buildscript {
	ext {
		restdocsApiSpecVersion = '0.17.1' // restdocsApiSpecVersion 버전 변수 설정
	}
}

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.4'
	id 'io.spring.dependency-management' version '1.1.6'
	id 'org.asciidoctor.jvm.convert' version '3.3.2'
	// epages-restdocs 플러그인 추가
	id 'com.epages.restdocs-api-spec' version "${restdocsApiSpecVersion}"
	//swagger generator 플러그인 추가
	id 'org.hidetake.swagger.generator' version '2.18.2'
}

// 5. 생성된 API 스펙이 어느 위치에 있는지 지정
swaggerSources {
	sample {
		setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
	}
}

// 6. openapi3 스펙 생성시 설정 정보
openapi3 {
	servers = [
			{ url = "http://localhost:8080" }
	]
	title = "API 문서"
	description = "RestDocsWithSwagger Docs"
	version = "0.0.1"
	format = "yaml"
}

dependencies {
	// Spring REST Docs 의존성
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
	// 8. openAPI3 추가
	testImplementation 'com.epages:restdocs-api-spec-mockmvc:' + restdocsApiSpecVersion
	// 9. SwaggerUI 추가
	swaggerUI 'org.webjars:swagger-ui:4.11.1'
}

// 10. openapi3가 먼저 실행 - doFrist를 통한 Header 설정 (글에서 자세하게 설명)
tasks.withType(GenerateSwaggerUI) {
	dependsOn 'openapi3'
	doFirst {
		def swaggerUIFile = file("${openapi3.outputDirectory}/openapi3.yaml")

		def securitySchemesContent =  "  securitySchemes:\n" +  \
                                      "    APIKey:\n" +  \
                                      "      type: apiKey\n" +  \
                                      "      name: Authorization\n" +  \
                                      "      in: header\n" + \
                                      "security:\n" +
				"  - APIKey: []  # Apply the security scheme here"

		swaggerUIFile.append securitySchemesContent
	}
}

// 11. 생성된 openapi3 스펙을 기반으로 SwaggerUISample 생성 및 static/docs 패키지에 복사
bootJar {
	dependsOn generateSwaggerUISample
	from("${generateSwaggerUISample.outputDir}") {
		into 'static/docs'
	}
}

로직은 build 후 -> jar 파일을 실행하면, static/resources 파일에 index.html이 생겨서 Swagger-UI가 적용됩니다.

 

[config/RestDocsConfiguration]

 

config/RestDocsConfiguration 파일은 RestDocs사용에 필요한 document 문법에 번거로운 헤더를 제거하고, prettyPrint()를 가지는 테스트 전용 빈 파일입니다.

Swagger-UI를 적용하기 위해서 MockMvcRestDocumentWrapper로 적용해야 합니다!

@TestConfiguration
public class RestDocsConfiguration {
    @Bean
    public RestDocumentationResultHandler restDocumentationResultHandler() {
        return MockMvcRestDocumentationWrapper.document(
                "{class-name}/{method-name}",  // 문서 이름 설정
                preprocessRequest(  // 공통 헤더 설정
                        modifyHeaders()
                                .remove("Content-Length")
                                .remove("Host"),
                        prettyPrint()),  // pretty json 적용
                preprocessResponse(  // 공통 헤더 설정
                        modifyHeaders()
                                .remove("Content-Length")
                                .remove("X-Content-Type-Options")
                                .remove("X-XSS-Protection")
                                .remove("Cache-Control")
                                .remove("Pragma")
                                .remove("Expires")
                                .remove("X-Frame-Options"),
                        prettyPrint())    // pretty json 적용
        );
    }
}

 

 

[userControllerTest]

기존에 적용했던 UserControllerTest 파일입니다. ResourceSnippetParameters.builder()를 적용하여, Swagger-UI에 필요한 tag, description 등을 추가할 수 있습니다. 모든 컨트롤러 테스트에 대해 작업을 시행하였습니다!

@Test
    @DisplayName("프로필 이미지를 변경한다.")
    @WithMockUser
    void changeProfile() throws Exception {
        // given
        ProfileChangeRequest profileChangeRequest = new ProfileChangeRequest("test.com");
        CustomUserDetails userDetails = CustomUserDetails.of(1L, "testUser",  AuthorityUtils.createAuthorityList("ROLE_USER"),"test12345");

        // when then
        mockMvc.perform(patch("/api/v1/users/profile")
                        .header("Bearer", "6ce1d11af9ac1adf97712c069ca33bc4564d675ce3958942bb3dc5601829881430cfd8d98c8745")
                        .with(SecurityMockMvcRequestPostProcessors.user(userDetails))
                        .content(objectMapper.writeValueAsString(profileChangeRequest))
                        .contentType(MediaType.APPLICATION_JSON)
                )
                .andDo(print())
                .andExpect(status().isOk())
                .andDo(restDocs.document(
                        resource(
                                ResourceSnippetParameters.builder()
                                        .tag("유저 API")
                                        .description("프로필 이미지 변경")
                                        .requestHeaders(
                                headerWithName("Bearer").description("발급 받은 엑세스 토큰입니다.")
                        )
                                        .requestFields(
                                fieldWithPath("imageUrl")
                                        .type(JsonType.STRING)
                                        .description("새로 변경할 프로필")
                                        .attributes(constraints("새로 변경할 프로필입니다."))
                        )
                                        .responseFields(
                                fieldWithPath("code")
                                        .type(JsonType.NUMBER)
                                        .description("성공시 반환 코드 (200)"),
                                fieldWithPath("status")
                                        .type(JsonType.STRING)
                                        .description("성공시 상태 값 (OK)"),
                                fieldWithPath("message")
                                        .type(JsonType.STRING)
                                        .description("성공 시 메시지 값 (OK)"),
                                fieldWithPath("data")
                                        .type(JsonType.STRING)
                                        .description("성공 시 반환 메시지")
                        ).build())));
    }

 

Swagger-UI로 확인해보기

빌드 후, jar 파일을 실행해보면, localhost:8080/docs/index.html에 스웨거 UI가 적용되어 있는것을 확인 할 수 있습니다!