REST Docs ๋„์ž…๊ธฐ

[#๐ŸฌBoard] API ๋ฌธ์„œ ๊ด€๋ฆฌ๋ฅผ ์ž๋™ํ™” ํ•˜๊ธฐ

ยท

3 min read

Board ํ”„๋กœ์ ํŠธ๋Š” ํ˜„์žฌ ํ”„๋ก ํŠธ ๋‹จ ์—†์ด ์„œ๋ฒ„ ๊ฐœ๋ฐœ๋งŒ ํ•˜๊ณ  ์žˆ๋Š” ์ƒํƒœ์ธ๋ฐ, ํ–ฅํ›„ ํ”„๋ก ํŠธ ๊ฐœ๋ฐœ์ž๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์˜ ๊ฐœ์š”๋ฅผ ๋ณด๊ณ  ์‹ถ์„ ๋•Œ ์„œ๋ฒ„ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ๋ˆˆ์— ๋ณด์ด๋Š” ๋ฌธ์„œ๋ผ๋„ ํ•„์š”ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ API ๋ฌธ์„œ ์ž๋™ํ™”์— ๋„๋ฆฌ ์“ฐ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, Swagger์™€ REST Docs ๋‘˜ ์ค‘ ํ•˜๋‚˜์˜ ์„ ํƒ์ง€๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” Swagger๋งŒ ์จ์˜ค๋˜ ํ„ฐ๋ผ REST Docs์—๋Š” ๋ฏธ์ˆ™ํ–ˆ์—ˆ๋Š”๋ฐ, ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  Board ํ”„๋กœ์ ํŠธ์— REST Docs๋ฅผ ๋„์ž…ํ•œ ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Swagger๋Š” ํ”„๋กœ๋•ํŠธ ์ฝ”๋“œ์— ์–ด๋…ธํ…Œ์ด์…˜์„ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๊ฑด ์น˜๋ช…์ ์ธ ๋‹จ์ ์ด๋ผ ์ƒ๊ฐ๋˜๋Š”๋ฐ, ๋งŒ์•ฝ Swagger ์ ์šฉ ์ดํ›„ ๋‹ค๋ฅธ API ๋ฌธ์„œ ์ž๋™ํ™” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์จ์•ผํ•  ๊ฒฝ์šฐ ํ”„๋กœ๋•ํŠธ ์ฝ”๋“œ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋’ค์ ธ๊ฐ€๋ฉฐ ์ˆ˜์ •ํ•ด์ค˜์•ผํ•˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • REST Docs๋Š” ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์„œ ์ „๋‹ฌ์˜ ๋ชฉ์ ์— ์ปดํŒฉํŠธํ•˜๊ฒŒ ์ž˜ ๋งž๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ๊ตณ์ด UI ๊ธฐ๋ฐ˜์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•  ํ•„์š”๋Š” ์—†์„ ๊ฑฐ๋ผ ์ƒ๊ฐ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’กbuild.gradle ๊ตฌ์„ฑ

plugins {
    ...
    # org.asciidoctor.jvm.convert ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ๋“ฑ๋ก
    # Asciidoctor๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ฌธ์„œ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
    id 'org.asciidoctor.jvm.convert' version "4.0.0-alpha.1"
}
configurations {
    ...
    # gradle์˜ configuration์— asciidoctorExt๋ผ๋Š” ์ƒˆ๋กœ์šด configuration์„ ์ถ”๊ฐ€
    asciidoctorExt
}
dependencies {
    ...
    # ์œ„์—์„œ ์ถ”๊ฐ€ํ•œ asciidoctorExt configuration์— ์˜์กด์„ฑ์„ ์ถ”๊ฐ€
    asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor'
    # mockmvc ๋ฅผ restdocs์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
    testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
tasks.named('test') {
    ...
    # test ํƒœ์Šคํฌ์—์„œ snippetsDir์„ ์•„์›ƒํ’‹ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์„ค์ •
    outputs.dir snippetsDir
}
ext {
    # snippetsDir์„ 'build/generated-snippets' ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์„ค์ •
    snippetsDir = file('build/generated-snippets') 
}
asciidoctor { 
    # Asciidoctor ํƒœ์Šคํฌ ์„ค์ •์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.
    inputs.dir snippetsDir # Asciidoctor ํƒœ์Šคํฌ๊ฐ€ snippetsDir ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›๋„๋ก ์„ค์ •
    configurations 'asciidoctorExt' # asciidoctorExt ๊ตฌ์„ฑ์„ ์‚ฌ์šฉ
    dependsOn test # test ํƒœ์Šคํฌ์— ์˜์กดํ•˜๋„๋ก ์„ค์ •
    baseDirFollowsSourceFile()
}
asciidoctor.doFirst {
    # Asciidoctor ํƒœ์Šคํฌ ์‹คํ–‰ ์ „์— ์ˆ˜ํ–‰ํ•  ์ž‘์—…์„ ์ •์˜
    # 'src/main/resources/static/docs/' ๋””๋ ‰ํ„ฐ๋ฆฌ์˜ ๋‚ด์šฉ์„ ์‚ญ์ œ
    delete file('src/main/resources/static/docs/*')
}
bootJar {
    # 'asciidoctor' ํƒœ์Šคํฌ์— ์˜์กด
    dependsOn asciidoctor
    copy {
        # Asciidoctor๋กœ ์ƒ์„ฑ๋œ ๋ฌธ์„œ๋ฅผ 'src/main/resources/static/docs/'๋กœ ๋ณต์‚ฌ
        from "${asciidoctor.outputDir}"
        into file("src/main/resources/static/docs")
    }
    copy {
        # 'src/main/resources/static' ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋‚ด์šฉ์„ 'build/resources/main/static'์œผ๋กœ ๋ณต์‚ฌ
        from file("src/main/resources/static")
        into file("build/resources/main/static")
    }
}

๋ฌธ์„œ ์ž๋™ํ™”๊ฐ€ Gradle ๋ผ์ดํ”„ ์‚ฌ์ดํด์— ์˜์กดํ•˜๋Š”๋งŒํผ, ์˜์กด์„ฑ ์ถ”๊ฐ€๋งŒ ํ•˜๋ฉด ๋˜๋Š” Swagger์— ๋น„ํ•ด Gradle ์„ค์ •์ด ํ›จ์”ฌ ๋ณต์žกํ•ฉ๋‹ˆ๋‹ค.

test ํƒœ์Šคํฌ์— ์˜์กดํ•˜๋Š” ์ปค์Šคํ…€ ํƒœ์Šคํฌ asciidoctor๋ฅผ ์ƒ์„ฑํ•˜๊ณ , bootJar ํƒœ์Šคํฌ๊ฐ€ asciidoctor ํƒœ์Šคํฌ๋ฅผ ์˜์กดํ•˜๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด test ํƒœ์Šคํฌ๊ฐ€ ํฌํ•จ๋œ ์‚ฌ์ดํด ์‹คํ–‰ ์‹œ, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ๋‚ด๋ถ€์—์„œ ์ž‘์„ฑํ–ˆ๋˜ API ๋ฌธ์„œ๊ฐ€ ์ž๋™์ ์œผ๋กœ ์•„์›ƒํ’‹ ๋””๋ ‰ํ† ๋ฆฌ์— ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๐Ÿ’กCommentControllerTest.java

class CommentControllerTest {
    ...
    @Test
    void ๋Œ“๊ธ€์„_์ž‘์„ฑํ•œ๋‹ค() throws Exception {
        CommentCreateRequest request = new CommentCreateRequest(VALUE);

        mockMvc.perform(post(DEFAULT_URI + "/posts/" + post.getId() + "/comments")
                .header("Authorization", "bearer " + accessToken)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(objectMapper.writeValueAsString(request)))
            .andExpect(status().isCreated())
            .andDo(document("commentCreate", 
                    preprocessRequest(prettyPrint()), 
                    preprocessResponse(prettyPrint())));
    }
}

๋Œ“๊ธ€์„ ์ž‘์„ฑํ•˜๋Š” ๊ฐ„๋‹จํ•œ API ํ…Œ์ŠคํŠธ ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค.

Rest Docs๋Š” ๋ฌธ์„œ ์ž‘์„ฑ ์ „์— ์š”์ฒญ๊ณผ ์‘๋‹ต์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ์ „์ฒ˜๋ฆฌ๊ธฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ „์ฒ˜๋ฆฌ๋Š” OperationRequestPreprocessor ๋˜๋Š” OperationResponsePreprocessor์™€ ํ•จ๊ป˜ document๋ฅผ ํ˜ธ์ถœํ•ด ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.

Preprocessors์— ์žˆ๋Š” ์Šคํƒœํ‹ฑ ๋ฉ”์†Œ๋“œ preprocessRequest, preprocessResponse๋กœ ์ธ์Šคํ„ด์Šค๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„, prettyPrint()๋กœ ์‘๋‹ต ์ปจํ…์ธ  ํ˜•์‹์„ ์ฝ๊ธฐ ์‰ฝ๊ฒŒ ๋ฐ”๊พธ์–ด์ค๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ API ์ŠคํŽ™๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ํ…Œ์ŠคํŠธ ๋นŒ๋“œ๋ฅผ ์‹คํŒจํ•˜๊ฒŒ ๋˜์–ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ๊ฒ€์ฆ๋œ ๋ฌธ์„œ๋ฅผ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

test ์ˆ˜ํ–‰ ์‹œ snippetsDir์œผ๋กœ ์„ค์ •ํ•ด์ฃผ์—ˆ๋˜ build/generated-snippets ๊ฒฝ๋กœ ํ•˜์œ„์— ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ์ž๋™์œผ๋กœ ๋ฌธ์„œํ™”๋œ http-request.adoc์„ ์‚ดํŽด๋ด…์‹œ๋‹ค.

perform()์œผ๋กœ ๋™์ž‘์‹œ์ผฐ๋˜ HTTP Request๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด์ œ ์ด๋ ‡๊ฒŒ ์ƒ์„ฑ๋œ adoc ํŒŒ์ผ์„ ์Šค๋‹ˆํŽซ์„ ์ด์šฉํ•˜์—ฌ ๋ฌธ์„œํ™”ํ•ด๋ด…์‹œ๋‹ค. index.adoc ํŒŒ์ผ์„ ์ƒ์„ฑ ํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์Šค๋‹ˆํŽซ์„ ์ž‘์„ฑํ•˜์˜€์Šต๋‹ˆ๋‹ค. asciidocs ๋ทฐ์–ด๊ฐ€ ์žˆ์œผ๋ฉด ํŽธํ•˜๊ฒŒ ์ฝ์„ ์ˆ˜ ์žˆ๋Š” ๋ฌธ์„œ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

= API
:doctype: book
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:seclinks:

== ๋Œ“๊ธ€ ์ž‘์„ฑ
Request
include::{snippets}/commentCreate/http-request.adoc[]
Response
include::{snippets}/commentCreate/http-response.adoc[]

๊ทธ๋Ÿฐ๋ฐ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ด adoc ํŒŒ์ผ๋งŒ์„ ์ „๋‹ฌํ•˜๊ธฐ๋ณด๋‹ค, html๋กœ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ํŽธํ•˜๊ฒŒ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์ด ๋ฌธ์„œํ™”์˜ ๋ชฉ์ ์ž…๋‹ˆ๋‹ค. ์ตœ์ข…์ ์œผ๋กœ, gradle ์„ค์ •์— ๋”ฐ๋ผ ์ƒ์„ฑ๋œ index.html ํŒŒ์ผ์„ API ๋ฌธ์„œ๋กœ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ์Šต๋‹ˆ๋‹ค.

REST Docs์— ๋Œ€ํ•œ ๋” ์ž์„ธํ•œ ์‚ฌ์šฉ๋ฒ•์€ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.


๐ŸŽฏ์ •๋ฆฌ

  • ํ”„๋กœ๋•ํŠธ ์ฝ”๋“œ๋ฅผ ์นจ๋ฒ”ํ•˜๋ฉฐ API๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„์˜ Swagger์™€ ๋‹ฌ๋ฆฌ REST Docs๋Š” ์ปดํŒฉํŠธํ•˜๊ฒŒ ๋ฌธ์„œํ™”๋งŒ์„ ๋ชฉ์ ์œผ๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ข€ ๋” ๊น”๋”ํ•ฉ๋‹ˆ๋‹ค.

  • REST Docs ๋ฌธ์„œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•ด์•ผ๋งŒ ์ž‘์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ฌธ์„œ๊ฐ€ ๊ฒ€์ฆ๋๋‹ค๋Š” ๊ฒƒ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ”–์ฐธ๊ณ 

ย