๐Ÿค” ์ ์šฉ ์ด์œ 

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋Š” ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž‘์„ฑํ•œ api ๋ฌธ์„œ๋ฅผ ๋ณด๊ณ  api ๋ฅผ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์€ ๋…ธ๋™๋ ฅ์ด ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ์ด๊ณ , ์‚ฌ๋žŒ์ด ์ž‘์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ณ€๊ฒฝ๋œ ์‚ฌํ•ญ์„ ์—…๋ฐ์ดํŠธ๋ฅผ ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๋„ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. spring-rest-docs ๋Š” ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ํ•˜๊ณ , ๋นŒ๋“œ ์‹œ api ๋ฌธ์„œ๊ฐ€ ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ฉด์„œ ๊ฒ€์ฆ๋œ api ๋ฌธ์„œ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค!

vs Swagger

Swagger ๋„ ๋ฌธ์„œํ™”๋ฅผ ์œ„ํ•ด ๋งŽ์ด ์‚ฌ์šฉ๋œ๋‹ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ณด๋‹ค UI ๊ฐ€ ๊น”๋”ํ•ด๋ณด์ด๋Š” ์žฅ์ ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. spring-rest-docs ์™€ ๋‹ฌ๋ฆฌ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ์˜๋ฌด๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ ๋น ๋ฅธ ์‹œ๊ฐ„ ๋‚ด์˜ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ ์šฉ์ดํ•  ๊ฑฐ๋ผ ์ƒ๊ฐ์ด ๋“ญ๋‹ˆ๋‹ค. ๋‹จ์ ์œผ๋กœ๋Š” ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ ์ฃผ์œ„์— ๋ฌธ์„œ๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์•ผ ๋œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ํŒ๋‹จ๋˜์–ด spring-rest-docs ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.


โš™๏ธ ์ ์šฉ ํ•˜๊ธฐ

1. build.gradle


plugins {
	id "org.asciidoctor.jvm.convert" version "3.3.2"
}

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

dependencies {
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}

test {
	outputs.dir snippetsDir
}

asciidoctor {
	dependsOn test
	inputs.dir snippetsDir
}

asciidoctor.doFirst {
	delete file('src/main/resources/static/docs') // -- 2 --
}

task copyDocument(type: Copy) {
	dependsOn asciidoctor
	from file("build/docs/asciidoc")
	into file("src/main/resources/static/docs") // -- 3 --
}

build {
	dependsOn copyDocument
}
  1. build ์‹œ ์ž๋™์ƒ์„ฑ๋  snippet ์˜ ์ €์žฅ ๊ฒฝ๋กœ (ex. request-body)
  2. asciidoctor ๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์šฐ์„ ์ ์œผ๋กœ ์ €์žฅ๋œ ๋ฌธ์„œ๋ฅผ ์‚ญ์ œํ•œ๋‹ค. (์ดˆ๊ธฐํ™”)
  3. asciidoctor ์‹คํ–‰ ์‹œ ์ƒ์„ฑ๋˜๋Š” index.html ์„ ์ •์  ๊ฒฝ๋กœ๋กœ ์ด๋™

2. MockMvc & restdocs ์„ค์ •

@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class SpringContainerTest {

    protected MockMvc mockMvc;

    @BeforeEach
    public void setUp(WebApplicationContext webApplicationContext,
                      RestDocumentationContextProvider restDocumentationContextProvider) throws Exception {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(documentationConfiguration(restDocumentationContextProvider))
                .build();
    }
}

๊ธฐ์กด ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ์—์„œ ์ถ”๊ฐ€๋œ ๋ถ€๋ถ„์€ apply(documentationConfiguration(restDocumentationContextProvider)) ์ž…๋‹ˆ๋‹ค.

RestDocumentationContextProvider: RestDocumentationContext ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค ์ ‘๊ทผ์„ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค RestDocumentationContext: RESTful API์˜ ๋ฌธ์„œํ™”๊ฐ€ ์ˆ˜ํ–‰๋˜๋Š” ์ปจํ…์ŠคํŠธ ์ธํ„ฐํŽ˜์ด์Šค


3. ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ

public class GroupControllerTest extends SpringContainerTest {

    @Test
    @Transactional
    @DisplayName("๊ทธ๋ฃน ๋ฆฌ์ŠคํŠธ ์กฐํšŒ")
    void getGroupList() throws Exception {

        mockMvc.perform(get("/api/groups")
                                .contentType(MediaType.APPLICATION_JSON)
                )
                .andExpect(status().isOk())
                .andDo(document("get-groups",
                        PayloadDocumentation.responseFields(
                                PayloadDocumentation.fieldWithPath("success").description("์„ฑ๊ณต ์—ฌ๋ถ€"),
                                PayloadDocumentation.fieldWithPath("result.[].groupId").description("๊ทธ๋ฃน ์•„์ด๋””"),
                                PayloadDocumentation.fieldWithPath("result.[].groupName").description("๊ทธ๋ฃน ์ด๋ฆ„"),
                                PayloadDocumentation.fieldWithPath("result.[].totalUsers").description("๊ทธ๋ฃน ์ธ์›์ˆ˜")
                        )));
    }

}

์ƒ์†๋ฐ›์€ MockMvc ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์š”์ฒญ ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†๊ธฐ ๋•Œ๋ฌธ์— ์‘๋‹ต ํ•„๋“œ๊ฐ’๊ณผ ์„ค๋ช…๊ฐ’์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.


4. ๋ฌธ์„œ ์Šค๋‹ˆํŽซ ์ƒ์„ฑ ํ™•์ธ

์ž‘์„ฑํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋๋‚ฌ๋‹ค๋ฉด, build ํด๋”๊ฐ€ ์ƒ๊ธธ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์š”์ฒญ ํ•„๋“œ ๊ฐ’์€ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ request-fileds ๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€ ์Šค๋‹ˆํŽซ์ด ์ƒ์„ฑ๋์Šต๋‹ˆ๋‹ค.

my@notebook build % tree
.
โ””โ”€โ”€ generated-snippets
    โ””โ”€โ”€ get-groups
        โ”œโ”€โ”€ curl-request.adoc
        โ”œโ”€โ”€ http-request.adoc
        โ”œโ”€โ”€ http-response.adoc
        โ”œโ”€โ”€ httpie-request.adoc
        โ”œโ”€โ”€ request-body.adoc
        โ”œโ”€โ”€ response-body.adoc
        โ””โ”€โ”€ response-fields.adoc

3 directories, 7 files

5. index.adoc ํŒŒ์ผ ์ž‘์„ฑ

src/docs/asciidoc/index.adoc ํŒŒ์ผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ์— ๋„ฃ๊ณ  ์‹ถ์€ ์Šค๋‹ˆํŽซ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. asciidoc ๊ฐ€ adoc ํŒŒ์ผ์„ ์Šคํƒ€์ผ๋ง ํ•ด์„œ html ํŒŒ์ผ๋กœ ๋ณ€ํ™˜์‹œ์ผœ์ค๋‹ˆ๋‹ค.

ifndef::snippets[]
:snippets: ./build/generated-snippets
endif::[]

= API Docs
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 1
:toc-title: ๊ทธ๋ฃน

== ๊ทธ๋ฃน ๋ฆฌ์ŠคํŠธ ์กฐํšŒ
=== REQUEST

include::{snippets}/get-groups/http-request.adoc[]

=== REQEUST FIELD

// include::{snippets}/get-groups/request-fields.adoc[]F

=== RESPONSE

include::{snippets}/get-groups/http-response.adoc[]

=== RESPONSE FIELD

include::{snippets}/get-groups/response-fields.adoc[]

6. build ๋ฐ ๋ฌธ์„œ ํ™•์ธ

๋ฌธ์„œ ์ €์žฅ๊ฒฝ๋กœ์ธ src/main/resources/static/docs ์— index.html ์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. http://localhost:8080/docs/index.html ๋กœ ์ด๋™ํ•˜๋ฉด..!

๋งŒ์•ฝ ์Šค๋‹ˆํŽซ์˜ ์ •๋ณด๊ฐ€ ์ž˜ ๋‚˜์˜ค์ง€ ์•Š๋Š”๋‹ค๋ฉด index.adoc ํŒŒ์ผ์—์„œ ์ž‘์„ฑํ•œ ์Šค๋‹ˆํŽซ ๊ฒฝ๋กœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“„ ๋งˆ์น˜๋ฉฐ..

์ดˆ๊ธฐ ์„ค์ •๋งŒ ํ•˜๋ฉด ์ดํ›„ ๋ฌธ์„œํ™”๋ฅผ ํ•˜๋Š” ๊ฒƒ์€ (ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค๋Š” ์ „์ œ) ์–ด๋ ต์ง€ ์•Š์•„ ๋ณด์ž…๋‹ˆ๋‹ค. ํ˜‘์—…ํ•˜๋Š” ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๊ฒ€์ฆ๋œ api๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„ํžˆ ๊ฐ€์น˜๊ฐ€ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ์กด์— ์‚ฌ์šฉํ•˜๋˜ ๋…ธ์…˜์— ๋น„ํ•ด์„œ ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๋‹จ์ ์€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž๋“ค์€ swagger ui ๋ฅผ ์—ฐ๋™ํ•ด์„œ ์ด๋ฅผ ๋ณด์™„ํ•œ๋‹ค๊ณ  ํ•ด์„œ ์ดํ›„์— ์ ์šฉํ•˜๋ ค ํ•ฉ๋‹ˆ๋‹ค.


์ฐธ๊ณ  ์‚ฌ์ดํŠธ

https://velog.io/@bagt/API-%EB%AC%B8%EC%84%9C%ED%99%94%EC%99%80-Spring-Rest-Docs