Spring Rest Docs (2) - API 문서에 디자인 입히기 (swagger, redoc)

Spring Rest Docs로 나온 Snippet들을 이용하여 API 문서를 만들 때, adoc 파일을 작성하여 만드는 방법 이외에 swagger 혹은 redoc을 이용하여 만드는 방법이 있습니다. 이 방법들을 사용하면 따로 adoc 파일을 작성하지 않아도 된다는 큰 장점이 존재해서 빠르게 API 문서를 만들어야 한다면 도입해 볼만하다고 생각합니다.

 

 

1. restdocs-api-spec 설정 추가

`swagger`나 `redoc`을 사용하기 위해서 기존의 Snippet들을 json으로 만들어 줘야하는데 이를 자동으로 변환하기 위해 `restdocs-api-spec` 플러그인 설정을 Gradle 빌드 스크립트에 추가한다. restdocs-api-spec의 장점은 Spring Rest Docs 기반으로 작성된 API 문서 코드를 아래와 같이 수정만 해주면 된다는 점이다. 이 기능을 이용하기 위해서는 빌드 스크립트를 수정해야한다.

// 기존 Rest Docs 기반 Snippet 생성 코드
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;

document(
    "identifier",
    requestFields(...),
    responseFields(...)
)


// restdocs-api-spec
import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.*;

document(
    "identifier",
    requestFields(...),
    responseFields(...)
)

 

https://github.com/ePages-de/restdocs-api-spec?tab=readme-ov-file#getting-started

 

GitHub - ePages-de/restdocs-api-spec: Adds API specification support to Spring REST Docs

Adds API specification support to Spring REST Docs - ePages-de/restdocs-api-spec

github.com

 

빌드 스크립트를 공식문서를 바탕으로 다음과 같이 설정해 주었다.

 

`build.gradle`

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.5.3'
	id 'io.spring.dependency-management' version '1.1.7'
	id "org.asciidoctor.jvm.convert" version "3.3.2"
	id 'com.epages.restdocs-api-spec' version '0.18.2'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	asciidoctorExt
}

repositories {
	mavenCentral()
}

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

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

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

	//restdocs-api-spec
	testImplementation('com.epages:restdocs-api-spec-mockmvc:0.18.2')
}

tasks.named('test') {
	useJUnitPlatform()
	outputs.dir snippetsDir
}

tasks.named('asciidoctor') {
	inputs.dir snippetsDir
	configurations 'asciidoctorExt'
	dependsOn tasks.test
}

bootJar {
	dependsOn tasks.asciidoctor
	dependsOn 'openapi3'

	from ("${asciidoctor.outputDir}") {
		into 'static/docs'
	}
	from("build/api-spec/openapi3.yaml") {
		into 'static/docs/api'
	}
	from("src/docs/redoc/index.html") {
		into 'static/docs/redoc/api'
	}
	from("src/docs/swagger/index.html") {
		into 'static/docs/swagger/api'
	}
}

bootRun {
	dependsOn tasks.asciidoctor
	dependsOn 'openapi3'
	classpath += files(tasks.asciidoctor.outputDir)

	doFirst {
		copy {
			from("src/docs/redoc/index.html")
			into("${tasks.asciidoctor.outputDir}/static/docs/redoc")
		}
		copy {
			from("src/docs/swagger/index.html")
			into("${tasks.asciidoctor.outputDir}/static/docs/swagger")
		}
		copy {
			from("build/api-spec/openapi3.yaml")
			into("${tasks.asciidoctor.outputDir}/static/docs")
		}
	}

	systemProperty "spring.web.resources.static-locations",
			"classpath:/static/,file:${tasks.asciidoctor.outputDir}"
}

openapi3 {
	server = 'https://localhost:8080'
	title = 'My API'
	description = 'My API description'
	version = '0.1.0'
	format = 'yaml'
}

 

./gradelw openapi3

위 명령어를 통해 OAS(Open API Specification)을 지키는 build/api-spec/openapi3.yaml 파일이 만들어 진다. 따라서, bootRun과 build task에 이 파일을 static/docs으로 복사하는 작업을 추가했다. 

 

2. UI 라이브러리 선택하기 (Swagger, Redoc ...)

위에서 만든 yaml 파일과 javascript cdn을 이용하면 UI 템플릿을 간단하게 적용할 수  있다. redoc과 swagger 공식문서들을 살펴보면 둘다 cdn으로 OAS yaml파일을 보기 좋은 API 문서로 만드는 방법을 소개한다.

 

https://github.com/Redocly/redoc

 

GitHub - Redocly/redoc: 📘 OpenAPI/Swagger-generated API Reference Documentation

📘 OpenAPI/Swagger-generated API Reference Documentation - Redocly/redoc

github.com

https://github.com/swagger-api/swagger-ui/blob/HEAD/docs/usage/installation.md

 

swagger-ui/docs/usage/installation.md at 2ab53f4f501e5a4a9e6cb031f49732be9ae28678 · swagger-api/swagger-ui

Swagger UI is a collection of HTML, JavaScript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API. - swagger-api/swagger-ui

github.com

 

이 글들을 참고하여 다음 디렉토리 구조로 index.html을 생성해 주었다.

 

`src/docs/redoc/index.html`

<redoc spec-url="../openapi3.yaml"></redoc>
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>

 

`src/docs/swagger/index.html`

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="SwaggerUI" />
    <title>SwaggerUI</title>
    <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@5.11.0/swagger-ui-bundle.js" crossorigin></script>
<script>
    window.onload = () => {
        window.ui = SwaggerUIBundle({
            url: '../openapi3.yaml',
            dom_id: '#swagger-ui',
        });
    };
</script>
</body>
</html>

 

redoc을 사용하는경우 request body와 response body의 json이 문자열 평문으로 나오는 문제가 있는데, gradle task를 추가해서 해결하였습니다. 아래 task를 통해 실제 나온 openapi3.yaml 파일의 value: 를 수정하였습니다.

 

`gradle task`

import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper

def yamlMapper = new YAMLMapper()
def jsonMapper = new ObjectMapper()

tasks.register('fixOpenApiExamples') {
	dependsOn tasks.openapi3
	doLast {
		def file = file("build/api-spec/openapi3.yaml")
		def root = yamlMapper.readTree(file)

		root.findParents('value')
				.each { JsonNode parent ->
					JsonNode v = parent.get('value')
					if (v?.isTextual()) {
						String txt = v.asText().trim()
						if (txt.startsWith('{') || txt.startsWith('[')) {
							try {
								((ObjectNode) parent)
										.set('value', jsonMapper.readTree(txt))
							} catch (ignored) {}
						}
					}
				}
		yamlMapper.writeValue(file, root)
	}
}

 

bootRun을 실행하기 전에 다음과 같이 fixOpenApiExamples 작업을 실행시키면 아래처럼 바뀌게 됩니다.

./gradlew fixOpenApiExamples bootRun

 

 

Swagger의 경우에도 다음과 같이 접속하면 우리가 자동으로 생성하던 그 템플릿을 그대로 볼 수 있다.

http://localhost:8080/docs/swagger/index.html

 

3. 목차(side bar) 수정하기

redoc같은 경우 기본적으로 왼쪽에 목차까지 제공하는데 해당 목차를 ui에 띄우는 방식이 가장 처음으로 나오는 uri를 기준으로 분류한다.

따라서, /api/items 의 경우 아래처럼 하나로 묶이게 된다.

`MockMvcRestDocumentation.document()` 메서드를 `MockMvcRestDocumentationWrapper.document()`로만 바꿨을 때는 deafult 설정으로 /api/** 일 때 api묶이게 된다.

 

이를 원하는 그룹으로 묶기 위해서는 시작 uri의 기준점을 재설정하거나 혹은 tag를 달아주는 것이다. tag를 달아주는 방식은 다음과 같다.

document(
        "items/create-item",
        resource(
                ResourceSnippetParameters.builder()
                        .tag("Item")
                        .summary("아이템 생성")
                        .description("아이템을 생성한다.")
                        .requestFields(
                                fieldWithPath("name").type(JsonFieldType.STRING).description("아이템 이름"),
                                fieldWithPath("description").type(JsonFieldType.STRING).description("아이템 부가 설명").optional(),
                                fieldWithPath("price").type(JsonFieldType.NUMBER).description("아이템 가격")
                        ).
                        responseFields(
                                fieldWithPath("id").type(JsonFieldType.NUMBER).description("아이템 id"),
                                fieldWithPath("name").type(JsonFieldType.STRING).description("아이템 이름"),
                                fieldWithPath("description").type(JsonFieldType.STRING).description("아이템 부가 설명"),
                                fieldWithPath("price").type(JsonFieldType.NUMBER).description("아이템 가격")
                        ).build()
        )
);

 

기존의 스니펫들을 ResourceSnippetParameters로 묶어주며 tag를 달아주면 해당 tag로 묶여서 관리되게 된다.

 

4. 마무리하며

restdocs-api-spec을 이용하여 OAS를 지키는 yaml파일을 생성하여 이를 redoc과 swagger를 이용해서 보기 좋은 API 문서로 만드는 방법을 시도해보았습니다. Swagger의 디자인도 나쁘지 않지만 저는 redoc ui가 더 가독성이 좋다고 생각합니다. 이런 디자인 같은 경우 개인 취향이기 때문에 자신이 원하는 디자인을 선택해서 사용하면 좋을것 같습니다.