ํด๋น ๊ธ์ ์๋ฅ์๋ฅ ๊ธฐ์ ๋ธ๋ก๊ทธ์ ์์ฑ๋ ๊ธ๊ณผ ๋์ผํฉ๋๋ค.
์ ํฌ ์๋ฅ์๋ฅ ํ์ ๊ธฐ์กด์ API
๋ช
์ธ๋ฅผ notion
์ ํตํด ์์ฑํ์ต๋๋ค.
ํ์ง๋ง ์ค์ ์ฝ๋์ ์ฐ๋๋์ง ์๋ ์ ์ด ๋ถํธํ๊ณ , API๋ณ ํ
์คํธ๋ฅผ ํตํ ๊ฒ์ฆ์ ํ๊ณ ์ถ์์ต๋๋ค.
์ด์ ์ ๋ขฐ์ฑ์๋ API
๋ฌธ์๋ฅผ ํ๋ก ํธ์๋์ ๊ณต์ ํ ์ ์๋ REST Docs
๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค.
๋ค์์ REST Docs
๋ฅผ ์ธํ
ํ๊ณ ๋ฌธ์ํ ํ ๊ณผ์ ์
๋๋ค.
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id "org.asciidoctor.jvm.convert" version "3.3.2"
}
group = 'com.wooteco'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
asciidoctorExtensions // dependencies ์์ ์ ์ฉํ ๊ฒ ์ถ๊ฐ
}
repositories {
mavenCentral()
}
ext {
// ์ฌ์ฉํ ๋ณ์ ์ ์ธ
set('snippetsDir', file("build/generated-snippets"))
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.3'
implementation 'com.google.guava:guava:31.0.1-jre'
// build/generated-snippets ์ ์๊ธด .adoc ์กฐ๊ฐ๋ค์ ํ๋ก์ ํธ ๋ด์ .adoc ํ์ผ์์ ์ฝ์ด๋ค์ผ ์ ์๋๋ก ์ฐ๋
// .adoc ํ์ผ์ HTML๋ก ๋ง๋ค์ด export
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:4.4.0'
testImplementation 'io.rest-assured:spring-mock-mvc:4.4.0'
testImplementation 'org.mockito:mockito-inline:3.8.0'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
test {
// ์์์ ์์ฑํ snippetsDir ๋๋ ํ ๋ฆฌ๋ฅผ test์ output์ผ๋ก ๊ตฌ์ฑํ๋ ์ค์
// -> ์ค๋ํซ ์กฐ๊ฐ๋ค์ด build/generated-snippets๋ก ์ถ๋ ฅ
outputs.dir snippetsDir
// ํ
์คํธ ์คํ
useJUnitPlatform()
}
// asciidoctor ์์
๊ตฌ์ฑ
asciidoctor {
// ์์์ ์์ฑํ configuration ์ ์ฉ
configurations 'asciidoctorExtensions'
// source๊ฐ ์์ผ๋ฉด .adocํ์ผ์ ์ ๋ถ html๋ก ๋ง๋ค์ด๋ฒ๋ฆผ
// source ์ง์ ์ ํน์ adoc๋ง HTML๋ก ๋ง๋ ๋ค.
sources{
include("**/index.adoc","**/common/*.adoc")
}
// ํน์ .adoc์ ๋ค๋ฅธ adoc ํ์ผ์ ๊ฐ์ ธ์์(include) ์ฌ์ฉํ๊ณ ์ถ์ ๊ฒฝ์ฐ ๊ฒฝ๋ก๋ฅผ baseDir๋ก ๋ง์ถฐ์ฃผ๋ ์ค์
// (๊ฐ๋ณ adoc์ผ๋ก ์ด์ํ๋ค๋ฉด ํ์ ์๋ ์ต์
)
baseDirFollowsSourceFile()
// snippetsDir ๋ฅผ ์
๋ ฅ์ผ๋ก ๊ตฌ์ฑ
inputs.dir snippetsDir
// asciidoctor ์ test ์คํ!
dependsOn test
}
// static/docs ํด๋ ๋น์ฐ๊ธฐ
asciidoctor.doFirst {
delete file('src/main/resources/static/docs')
}
// asccidoctor ์์
์ดํ ์์ฑ๋ HTML ํ์ผ์ static/docs ๋ก copy
task createDocument(type: Copy) {
dependsOn asciidoctor
from file("build/docs/asciidoc")
into file("src/main/resources/static")
}
// build ์ createDocument(REST Docs ๋ฌธ์ํ) ์คํ!
build {
dependsOn createDocument
}
Gradle
7.4.1 ๋ฒ์
grovvy
๋ฌธ๋ฒ์ ์ ํํ ์์ง ๋ชปํด ํ๋ฆฐ ๋ถ๋ถ์ด ์์ ์ ์์ต๋๋ค.
build/generated-snippets
์ ์ ์ฅstatic/index.html
๋น์ฐ๊ธฐsrc/docs/asciidoc/index.adoc
์ ํตํด build/docs/asciidoc/
์ index.html
์์ฑHTML
ํ์ผ์ build
์์ src/main/resources/static/
๋ก ์ด๋ํ๋ค์ฌ์ค ์ด๋ index.html
์ด ์์ฑ๋๋ ์ฃผ๊ธฐ๋ฅผ ์ธ์ ๋ก ํ ์ง ๊ณ ๋ฏผํ์ต๋๋ค.
build
๋จ์๋ก ์ฌ์์ฑ์ ํ ์ build
์๊ฐ์ด ๊ธธ์ด์ง๊ธฐ ๋๋ฌธ์, test
๋ง๋ค ์คํ ์๊ฐ์ด ๊ธธ์ด์ง๋ค๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
asciidoctor
๋ช
๋ น์ด๋ฅผ ์ด์ฉํด ์๋ export
๋ฅผ ํ๋ ๋ฐฉ์๋ ๊ณ ๋ คํ์ง๋ง, ์์ง ์คํ ์๊ฐ์ด ๊ธธ์ด์ง ์ ๋๊ฐ ํฌ์ง ์์ ํ์ฌ ์ธํ
์ ์ ์งํ๊ณ ์์ต๋๋ค.
RestAssuredMockMvc
Spring
์ย MockMvc
ย ์์ ๋น๋๋ REST
API
์ธ RestAssuredMockMvc
๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
RestAssured
์ ๋ฌธ๋ฒ์ด ๊ฐ๊ณ , @WebMvcTest
ํํ์ ํ
์คํธ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
ํธํ ๋ฌธ๋ฒ์ผ๋ก ๋น ๋ฅด๊ฒ ํ
์คํธํ๊ธฐ ์ํด ํด๋น API
๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
๊ณต์๋ฌธ์
RestAssuredMockMvc - spring-mock-mvc 2.8.0 javadoc
baeldung
ControllerTest
)import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint;
// ... ๋ฑ๋ก๋ ์ปจํธ๋กค๋ฌ/ MockBean ์ํฌํธ
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@WebMvcTest({
PostController.class,
// ... ํ
์คํธํ ์ปจํธ๋กค๋ฌ ๋ฑ๋ก
})
@ExtendWith(RestDocumentationExtension.class)
public class ControllerTest {
protected MockMvcRequestSpecification restDocs;
@MockBean
protected PostService postService;
// ... MockBean ๋ฑ๋ก
// RestDocs ์ธํ
@BeforeEach
void setRestDocs(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
restDocs = RestAssuredMockMvc.given()
.mockMvc(MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withRequestDefaults(prettyPrint())
.withResponseDefaults(prettyPrint()))
.build())
.log().all();
}
}
class HashtagControllerTest extends ControllerTest { [// (0)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
@DisplayName("ํด์ํ๊ทธ๋ก ๊ฒ์ ์ ์๋ ํด์ํ๊ทธ์ด๋ฉด 404 ๋ฐํ")
@Test
void findPostsWithHashtags_Exception_NoHashtag() {
// Mockito๋ก ๋ฑ๋ก๋ MockBean ์ธํ
doThrow(new HashtagNotFoundException())
.when(hashtagService)// [(1)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
.findPostsWithHashtag(matches("์๋ํ๊ทธ"), any());
// ํ
์คํธ ์ํ
restDocs
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when().get("/posts?hashtag=์๋ํ๊ทธ&size=5&page=0")
.then().log().all()
.assertThat() [// (2)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
.apply(document("search/byHashtag/fail/noHashtag")) [// (3)](https://www.notion.so/REST-Docs-2f26a6e79ce045aa95a87445e2da4d38)
.statusCode(HttpStatus.NOT_FOUND.value());
}
(0) ์์ ControllerTest
๋ฅผ ์์ํฉ๋๋ค.
(1) hashtagService
๋ ControllerTest
์ @MockBean
์ผ๋ก ๋ฑ๋ก๋์ด ์์ต๋๋ค.
(2) ๊ฐ๋
์ฑ์ ๋๋ ์ฝ๋. ์๋ ๋ด์ฉ๋ค์ ๊ฒ์ฆํฉ๋๋ค.
(3) build/docs/asciidoc/
์์ ํด๋น ๋๋ ํ ๋ฆฌ์ ๊ฒฐ๊ณผ adoc
ํ์ผ๋ค์ด ์ ์ฅ๋ฉ๋๋ค.
๋น๋ํ๋ฉด build/generated-snippets
ํด๋์ ํ
์คํธ์์ ์์ฑํ board/create/fail
๋๋ ํ ๋ฆฌ์ adoc
์กฐ๊ฐ๋ค์ด ์์ฑ๋์ด ์์ต๋๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ ์กฐ๊ฐ๋ค์ด default
๋ก ์์ฑ๋ฉ๋๋ค.
curl-request.adoc
http-request.adoc
httpie-request.adoc
http-response.adoc
request body
response body
์ฐ๋ฆฌ๋ ์ด ์ค http-request.adoc
(์์ฒญ) , http-response.adoc
(์๋ต)์ ์ฌ์ฉํฉ๋๋ค. ์ด์ธ ํ์ผ๋ค์ ์ฌ์ฉํ์ง ์์ต๋๋ค.
index.adoc
๋ฌธ์ ์์ฑ์ฐ์ adoc
ํ์ผ ์์ฑ์ ํธ์๋ฅผ ์ํด AsciiDoc
ํ๋ฌ๊ทธ์ธ์ ์ค์นํฉ๋๋ค.
ํด๋น ํ๋ฌ๊ทธ์ธ์ผ๋ก adoc
๊ฒฐ๊ณผ๋ฌผ์ gui
ํํ๋ก ๋ณผ ์ ์๊ณ , ๋ฌธ๋ฒ ํ์ธ๋ ๋ ๊ฐํธํด์ง๋๋ค!
์ด์ API ๋ฌธ์๋ฅผ adoc
ํํ๋ก ์์ฑํ๋ฉด ๋ฉ๋๋ค.
src/docs/asciidoc/
๋๋ ํ ๋ฆฌ ์์ index.adoc
ํ์ผ์ ๋ง๋ค๊ณ ๋ฌธ์๋ฅผ ์์ฑํฉ๋๋ค.
(๋ ธ๊ฐ๋คโฆ)
= ์๋ฅ์๋ฅ API ๋ช
์ธ
:doctype: book
:icons: font
:source-highlighter: highlightjs // ์ฝ๋๋ค์ ํ์ด๋ผ์ดํ
์ highlightjs๋ฅผ ์ฌ์ฉ
:toc: left // Table Of Contents(๋ชฉ์ฐจ)๋ฅผ ๋ฌธ์์ ์ข์ธก์ ๋๊ธฐ
:toclevels: 2 // ๋ชฉ์ฐจ ๋ ๋ฒจ ์ค์
:sectlinks:
:sectnums: // ๋ถ๋ฅ๋ณ ์๋์ผ๋ก ์ซ์๋ฅผ ๋ฌ์์ค
:docinfo: shared-head
== ๋ฉ์ธํ์ด์ง // "="๋ ๋งํฌ๋ค์ด "#"๊ณผ ๊ฐ์ ๊ธฐ๋ฅ
=== ๋ฉ์ธํ์ด์ง ์กฐํ
==== ์ฑ๊ณต
operation::board/find/content[snippets='http-request,http-response'] // ํน์ adoc ํ์ผ ์๋ ์ํฌํธ
//... ์๋ ๋ถ๋ถ๋ค ์ง์ ์์ฑ
operation::๋๋ ํ ๋ฆฌ๋ช
[snippets=โ์ํ๋ ์กฐ๊ฐ๋คโ]
์์ธํ Asciidoc ์ฌ์ฉ๋ฒ ๋งํฌ
์ด์ ๋น๋ํ๋ฉด
์์ ์์ฑํ index.adoc
ํ์ผ์ด index.html
ํํ๋ก export
๋ฉ๋๋ค!
index.html
ํ์ผ์
build/docs/asciidoc/
, src/main/resources/static/
๋ ์นดํ
๊ณ ๋ฆฌ ์์ ์์ต๋๋ค.
( build.gradle
์ createDocument
๋ช
๋ น์ด๋ก ๋ณต์ฌํ์ต๋๋ค.)
์ด์ค ์ ํฌ๊ฐ ์น ์์ ๋ณด์ฌ์ค ๋ฌธ์๋ src/main/resources/static/
์์ ์์นํด ์์ต๋๋ค.
์ด์ src/main/resources/static/index.html
ํ์ผ์ pr
์ ๋ ๋ ค
๊นํ๋ธ ๋ ํฌ์ ๋ณํฉํฉ๋๋ค.
## ์๋ฅ์๋ฅ
[RESTDocs ๋ก ๊ตฌํ๋ API ๋ช
์ธ](backend/sokdak/src/main/resources/static/index.html)
README.md
์ ๋งํฌ๋ฅผ ๋จ๊ฒจ์ฃผ์๋ฉด
๋ค์๊ณผ ๊ฐ์ด index.html
์ ์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค.
์ข๋ ํธํ๊ฒ API ๋ฌธ์๋ฅผ ๋ณด๊ธฐ ์ํด github pages
๋ฅผ ์ด์ฉํฉ๋๋ค.
์ด๋
main
๋ธ๋์น๋ก ํ์ฑํํ๋ฉด main
๋ธ๋์น ์ index.html
๋กdev
๋ธ๋์น๋ก ํ์ฑํํ๋ฉด dev
๋ธ๋์น ์ index.html
๋ก์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค.
ํ์ฌ๋ dev
๋ธ๋์น๋ก ์ค์ ํด๋์์ต๋๋ค.
์ด์ https://woowacourse-teams.github.io/2022-sokdak/ ๋ก ๋ค์ด๊ฐ ์
README.md
๊ฐ ๋จ๊ณ
๋งํฌ๋ฅผ ํด๋ฆญ ์
์ธ์ ์ด๋์๋ ํด๋น ๋งํฌ๋ก API
๋ช
์ธ๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
๊ฐ๋ฐ์ ๋ฟ ์๋๋ผ ์ ์ ๋ค๋ ํด๋น API
๋ฌธ์์ ์ ๊ทผ ๊ฐ๋ฅํฉ๋๋ค.