Spring/[2024] Spring Boot

Spring REST Docs ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฌธ์„œ ์ž๋™ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ• + ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ ์ž‘์„ฑ (Spring +JPA)

์ดˆ๋ณด๋ณด ํ˜œ์ง„ 2024. 2. 27. 16:43
728x90

 

* ํ˜น์‹œ๋‚˜ ๋ฐ”๋กœ ์ฝ”๋“œ๋ถ€ํ„ฐ ๋ณด๊ณ  ์‹ถ์€ ์‚ฌ๋žŒ๋“ค์€ 
-> " Spring REST Docs ๋ฅผ ์ด์šฉํ•œ Sample Project " ๊ฒ€์ƒ‰ ๋ ˆ์ญˆ๊ณ  

 

 

 

0๏ธโƒฃ Spring REST Docs ๋ž€ ? 

: Spring MVC Test ๋˜๋Š” WebTestClient์œผ๋กœ ์ƒ์„ฑ๋œ ์Šค๋‹ˆํŽซ๊ณผ ์ง์ ‘ ์ž‘์„ฑํ•œ ๋ฌธ์„œ๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ Restful ์„œ๋น„์Šค๋ฅผ ๋ฌธ์„œํ™”ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. 

 

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction

 

Spring REST Docs

Document RESTful services by combining hand-written documentation with auto-generated snippets produced with Spring MVC Test or WebTestClient.

docs.spring.io

-> Spring REST Docs ๊ด€๋ จ ๊ณต์‹ ๋ฌธ์„œ์ด๋‹ค. ์„ค๋ช…์ด ์ž˜ ๋˜์–ด ์žˆ์–ด์„œ ์‚ฌ์‹ค ์—ฌ๊ธฐ์— ๋‚˜์˜จ๋Œ€๋กœ ๋”ฐ๋ผ์„œ ํ•˜๋ฉด ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. 

 

 

Spring REST Docs๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Asciidoctor๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. Asciidoctor๋Š” ์ผ๋ฐ˜ ํ…์ŠคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ํ•„์š”์— ๋งž๊ฒŒ ์Šคํƒ€์ผ๊ณผ ๋ ˆ์ด์•„์›ƒ์„ ๊ฐ–์ถ˜ HTML ์„ ์ƒ์„ฑํ•ด์ค€๋‹ค. (์—ฌ๊ธฐ์„œ๋Š” ์Šค๋‹ˆํŽซ์„ ๋ชจ์•„ ๊ฒฐํ•ฉํ•œ ๊ฒƒ์„ HTML ๋กœ ๋ณ€ํ™˜ํ•ด์ค€๋‹ค.)

Spring REST Docs ๋Š” Spring MVC์˜ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„ ์›Œํฌ, Spring WebTestClient ๋˜๋Š” REST Assured 5๋กœ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ์—์„œ ์ƒ์„ฑ๋œ ์Šค๋‹ˆํŽซ์„ ์‚ฌ์šฉํ•œ๋‹ค.

์ด ํ…Œ์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ๋ฐฉ์‹์€ ์„œ๋น„์Šค ๋ฌธ์„œ์˜ ์ •ํ™•์„ฑ์„ ๋ณด์žฅํ•˜๋Š”๋ฐ ๋„์›€์ด ๋œ๋‹ค. -> ์‹ ๋ขฐ์„ฑ !! ์™œ๋ƒํ•˜๋ฉด ํ…Œ์ŠคํŠธ์— ์‹คํŒจํ•˜๋Š” ์ผ€์ด์Šค๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์—†๋‹ค. 

 

Swagger ๋Œ€์‹  Spring REST Docs ๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ  ? 

์‚ฌ์‹ค ๋‘˜ ์ค‘  ๋ญ๊ฐ€ ๋” ๋‚ซ๋‹ค ! ๋ผ๊ณ  ๋ง์€ ํ•  ์ˆ˜ ์—†๋‹ค. ๋‘˜ ๋‹ค ์žฅ๋‹จ์ ์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. 

- Swagger๋Š” ์ ์šฉ์ด ์‰ฝ๊ณ , ๋ฌธ์„œ์—์„œ ๋ฐ”๋กœ ํ˜ธ์ถœํ•ด ํ…Œ์ŠคํŠธํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์— ์นจํˆฌ์ ์ด๋ฉฐ ํ…Œ์ŠคํŠธ์™€๋Š” ๋ฌด๊ด€ํ•ด์„œ ์‹ ๋ขฐ์„ฑ์€ ๋–จ์–ด์ง„๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค. 

- Spring REST Docs ๋Š” Swagger์™€ ๋‹ค๋ฅด๊ฒŒ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผํ•ด์•ผ ๋ฌธ์„œ๊ฐ€ ๋งŒ๋“ค์–ด์ง€๊ธฐ ๋•Œ๋ฌธ์— ์‹ ๋ขฐ๋„๊ฐ€ ๋†’๊ณ , ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌํ•ด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์นจํˆฌ์ ์ด์ง€ ์•Š๋‹ค. ํ•˜์ง€๋งŒ Swagger์— ๋น„ํ•˜๋ฉด ํ›จ์”ฌ ์ฝ”๋“œ์–‘์ด ๋งŽ๊ณ , ์„ค์ •์ด ์–ด๋ ต๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค. 

=> ์šฐ์„  ์ฒ˜์Œ์— ๋‚˜๋Š” Swagger๋ฅผ ํ†ตํ•ด ์ž‘์„ฑํ•˜๋ฉด ์‚ฌ์‹ค ์ฝ”๋“œ๋Š” ๊ฐ„ํŽธํ•˜๋‹ˆ๊นŒ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.
๊ทผ๋ฐ ๋ฌธ์„œ ์ปค์Šคํ…€์„ ์œ„ํ•ด์„œ๋Š” ์ ์  ๋ฉ”์ธ ์ฝ”๋“œ๋“ค์— ๋ถˆํ•„์š”ํ•œ ์–ด๋…ธํ…Œ์ด์…˜๋“ค์ด ๋“ค์–ด๊ฐ€๋‹ˆ๊นŒ ์ฝ”๋“œ๊ฐ€ ์ง€์ €๋ถ„ํ•ด ์ง€๋Š”๊ฒŒ ์‹ซ์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ  api ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์€๊ฒƒ์ด๋ฏ€๋กœ Spring REST Docs๊ฐ€ ๋” ๋‚ซ๋‹ค๋Š” ์ƒ๊ฐ์œผ๋กœ ๋ฐ”๋€Œ์—ˆ๋‹ค. 
๊ตณ์ด ์ง์ ‘ ํ˜ธ์ถœํ•ด ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๊ฒƒ์€ ์ธํ…”๋ฆฌ์ œ์ด์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ httpd ๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. (ํด๋ผ์ด์–ธํŠธ ์ž…์žฅ์€ ์•„๋‹ˆ์ง€๋งŒ) 

๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ ํšŒ์‚ฌ ๊ธฐ์ค€? ์ด๊ธฐ๋Š” ํ•˜์ง€๋งŒ api ๋ฌธ์„œ๋ฅผ v0.1 ์ด๋Ÿฐ์‹์œผ๋กœ ๋งŒ๋“œ๋Š”๋ฐ ์ˆ˜์ •ํ•˜๊ณ  ํŒŒ์ผ์„ ๊ณต์œ ํ•ด๋„ ์–ด๋–ค ์‚ฌ๋žŒ์€ ์•„์ง๋„ 0.1 ๋ฒ„์ „๋ณด๊ณ  ์žˆ๊ณ , ์–ด๋–ค ์‚ฌ๋žŒ์€ 0.2 ๋ฒ„์ „์„ ๋ณด๊ณ  ์žˆ๊ณ ,,? ์ตœ์‹  ๋ฌธ์„œ๋ฅผ ๋ณด์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์•˜๋‹ค. 
๋ฐ”๋น ์„œ ๋ฉ”์ผ ํ™•์ธ์„ ๋ฐ”๋กœ๋ฐ”๋กœ ๋ชปํ•˜๋ฉด ๊ทธ๋Ÿด ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋Ÿด๊ฑฐ๋ฉด ์ฝ”๋“œ ์ˆ˜์ • ํ›„ ํ•ญ์ƒ ํ…Œ์ŠคํŠธ ํ•˜๊ณ  ์„ฑ๊ณตํ•˜๋ฉด api ๋ฌธ์„œ๋„ ๋งŒ๋“ค์–ด์ง€๋‹ˆ๊นŒ ์ด๋ ‡๊ฒŒ ํ™•์ธํ•˜๋Š”๊ฒŒ ๋” ๋‚ซ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. 

์‚ฌ์‹ค ์„ค์ •์ด์•ผ ์ฒ˜์Œ์—๋งŒ ์–ด๋ ต๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  Asciidoc ๋ฌธ๋ฒ•์ด ์–ด๋ ต๋‹ค๊ณ  ๋А๊ปด์งˆ ์ˆ˜ ์žˆ๋‹ค. ๊ทผ๋ฐ ์ด๊ฑด ์‚ฌ์‹ค ์ฒ˜์Œ ํ•ด๋ด์„œ ๊ทธ๋Ÿฐ๊ฑฐ ์•„๋‹๊นŒ? ๋ผ๋Š” ์ƒ๊ฐ... ์ต์ˆ™ํ•ด์ง€๋ฉด ์ด๊ฒŒ ๋” ๋‚ซ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. 
์ฒ˜์Œ์—๋Š” ๋ฌผ๋ก  ์‹œ๊ฐ„๋„ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ , ์ฝ”๋“œ ์–‘๋„ ๋งŽ์•„์„œ ๊ท€์ฐฎ์•„์งˆ ์ˆ˜ ์žˆ์ง€๋งŒ, word๋กœ ํ‘œ๊ทธ๋ฆฌ๊ณ , ๊ฑฐ๊ธฐ์— ์„ค๋ช… ์“ฐ๊ณ  ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ด๊ฒŒ ํ›จ์”ฌ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค. (์ด๊ฑด ๋‚ด๊ฐ€ ์›Œ๋“œ ์ž‘์—…์„ ๋ณ„๋กœ ์•ˆ์ข‹์•„ํ•ด์„œ ์ผ์ˆ˜๋„...) 
์•”ํŠผ ์ด๋Ÿฐ ์ €๋Ÿฐ ์ด์œ ๋กœ ๋‚˜๋Š” Spring REST Docs๋ฅผ ์„ ํƒํ–ˆ๋‹ค. 

 

 

1๏ธโƒฃ Spring REST Docs ์‹œ์ž‘ํ•˜๊ธฐ + ์„ค์ • 

์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด์„œ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•, ๋ฌธ์„œ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•๋“ค์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณผ ์˜ˆ์ •์ด๋‹ค. 

 

์ตœ์†Œ ์š”๊ตฌ์‚ฌํ•ญ 
- ์ž๋ฐ” 17 
- ์Šคํ”„๋ง ํ”„๋ ˆ์ž„์›Œํฌ 6 

 

์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ ํ™˜๊ฒฝ 
- ์ž๋ฐ” 17 
- ์Šคํ”„๋ง ๋ถ€ํŠธ 3.2.2 
- JPA + H2
- Junit5  + MockMVC 
- AsciiDoc

 

 

build.gradle 

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

configurations {
	asciidoctorExt (2)
}

dependencies {
	asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' (3)
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' (4)
}

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

test { (6)
	outputs.dir snippetsDir
}

asciidoctor { (7)
	inputs.dir snippetsDir (8)
	configurations 'asciidoctorExt' (9)
	dependsOn test (10)
}

 

-> ๊ณต์‹ ๋ฌธ์„œ์— ๋‚˜์™€์žˆ๋Š” build.gradle ์„ค์ • ๋ฐฉ์‹์ด๋‹ค. 

 

(1) AsciiDoc ํŒŒ์ผ์„ ์ปจ๋ฒ„ํŒ…ํ•˜๊ณ  Build ํด๋”์— ๋ณต์‚ฌํ•˜๊ธฐ ์œ„ํ•œ ํ”Œ๋Ÿฌ๊ทธ์ธ์ด๋‹ค.

(2) Asciidoctor ๋ฅผ ํ™•์žฅํ•˜๋Š” ์ข…์†์„ฑ์— ๋Œ€ํ•œ ๊ตฌ์„ฑ์„ ์„ ์–ธํ•œ๋‹ค.

(3) asciidoctorExt ๊ตฌ์„ฑ์— spring-restdocs-asciidoctor์— ๋Œ€ํ•œ ์ข…์†์„ฑ์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด .adoc ํŒŒ์ผ์—์„œ ์‚ฌ์šฉํ•  snippets ์†์„ฑ์ด build/generated-snippets๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋„๋ก ์ž๋™์œผ๋กœ ๊ตฌ์„ฑ๋œ๋‹ค. ๋˜ํ•œ ์ž‘์—… ๋ธ”๋ก ๋งคํฌ๋กœ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

(4) mockmvc๋ฅผ restdocs์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ → ๋งŒ์•ฝ Rest Assured ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด ๋‹ค๋ฅธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉํ•ด์•ผํ•จ

(5) ์ƒ์„ฑ๋œ ์ฝ”๋“œ ์กฐ๊ฐ์˜ ์ถœ๋ ฅ ์œ„์น˜๋ฅผ ์ •์˜ํ•˜๋Š” snippetsDir ์†์„ฑ

(6) Gradle ์ด ํ…Œ์ŠคํŠธ ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด ์ถœ๋ ฅ์ด snippetsDir์— ๊ธฐ๋ก๋œ๋‹ค.

(7) asciidoctor task๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค.

(8) ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด snippetsDir์—์„œ ์ฝ๋Š”๋‹ค.

(9) ํ™•์žฅ์„ ์œ„ํ•ด asciidoctorExt ์„ ๊ตฌ์„ฑํ•œ๋‹ค.

(10) ํ…Œ์ŠคํŠธ๊ฐ€ ๋จผ์ € ์‹คํ–‰๋˜๊ณ  ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋˜๋„๋ก ํ•œ๋‹ค.

(11) gradle build ์‹œ asciidoctor ๋จผ์ € ์ˆ˜ํ–‰ ํ›„, bootJar ๊ฐ€ ์ˆ˜ํ–‰๋œ๋‹ค.

(12) gradle build ์‹œ build/generated-snippets/html5์— html ํŒŒ์ผ์ด ์ƒ๊ธฐ๊ณ , gradle build ์‹œ static/docs ํด๋”์— ๋ณต์‚ฌ๋œ๋‹ค.

 

์ˆœ์„œ : test -> asciidoctor -> bootJar 

 

 

ํ…Œ์ŠคํŠธ ์„ค์ • 

: Spring REST Dcos๋Š” Junit5 ๋ฐ Junit4๋ฅผ ์ง€์›ํ•˜๊ณ , Junit5 ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•œ๋‹ค. 

@ExtendWith(RestDocumentationExtension.class) // (1) 
public class JUnit5ExampleTests {
		private MockMvc mockMvc;
		
		@BeforeEach // (2) 
		void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
			this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
				.apply(documentationConfiguration(restDocumentation)) 
				.build();
		}
	}

 

(1) RestDocumentationExtension ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค ์ ์šฉ

(2) MockMvc ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด @BeforeEach ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•œ๋‹ค.

 

-> ์‚ฌ์‹ค ์ด๋ ‡๊ฒŒ ๋‘๊ฐœ๋งŒ ์„ค์ •ํ•ด์ฃผ๋ฉด ๊ธฐ๋ณธ ์„ค์ •์€ ๋‹ค ํ•œ ๊ฒƒ์ด๊ณ , ์ด์ œ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค. 

 

 

2๏ธโƒฃ Restful ์„œ๋น„์Šค ํ˜ธ์ถœ , ์š”์ฒญ ๋ฐ ์‘๋‹ต ๋ฌธ์„œํ™” 

 

Restful ์„œ๋น„์Šค ํ˜ธ์ถœ

this.mockMvc.perform(get("/").accept(MediaType.APPLICATION_JSON)) // (1) 
	.andExpect(status().isOk()) // (2) 
	.andDo(document("index")); // (3) 

(1) get ์š”์ฒญ์œผ๋กœ ์„œ๋น„์Šค์˜ ๋ฃจํŠธ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

(2) ์„œ๋น„์Šค๊ฐ€ ์˜ˆ์ƒํ•˜๋Š” ์‘๋‹ต์„ ์ƒ์„ฑํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

(3) ์„œ๋น„์Šค์— ๋Œ€ํ•œ ํ˜ธ์ถœ์„ ๋ฌธ์„œํ™”ํ•˜์—ฌ “index” ๋ผ๋Š” ๋””๋ ‰ํ„ฐ๋ฆฌ์— ์ฝ”๋“œ ์กฐ๊ฐ์„ ์ž‘์„ฑํ•œ๋‹ค. ์Šค๋‹ˆํŽซ์€ RestDocumentationResultHandler์— ์˜ํ•ด์„œ ์ž‘์„ฑ๋œ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ๊ธฐ๋ณธ์ ์œผ๋กœ 6๊ฐœ์˜ ์Šค๋‹ˆํŽซ์ด ์ƒ๊ธด๋‹ค.

  • <output-directory>/index/curl-request.adoc
  • <output-directory>/index/http-request.adoc
  • <output-directory>/index/http-response.adoc
  • <output-directory>/index/httpie-request.adoc
  • <output-directory>/index/request-body.adoc
  • <output-directory>/index/response-body.adoc

 

์Šค๋‹ˆํŽซ ์‚ฌ์šฉ

์ƒ์„ฑ๋œ ์Šค๋‹ˆํŽซ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์— ์†Œ์Šค ํŒŒ์ผ์„ ์ƒ์„ฑ ํ•ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜๋“ค์€ src/docs/asciidoc/(ํŒŒ์ผ๋ช…).doc ์œผ๋กœ ์†Œ์ŠคํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๋ฉด ๋œ๋‹ค.

์ƒ์„ฑ ํ›„,

include::{snippets}/index/curl-request.adoc[]

→ ์ด๋Ÿฐ์‹์œผ๋กœ include๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆ˜๋™์œผ๋กœ ์ƒ์„ฑ๋œ Asciidoc ํŒŒ์ผ์— ์ƒ์„ฑ๋œ ์กฐ๊ฐ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 API ๋ฌธ์„œํ™”

  • ์š”์ฒญ ๋ฐ ์‘๋‹ต ํ•„๋“œ
{
	"contact": {
		"name": "Jane Doe",
		"email": "jane.doe@example.com"
	}
}

this.mockMvc.perform(get("/user/5").accept(MediaType.APPLICATION_JSON))
	.andExpect(status().isOk())
	.andDo(document("index", responseFields( // (1) 
			fieldWithPath("contact.email").description("The user's email address"), // (2)
			fieldWithPath("contact.name").description("The user's name"))));

(1) org.springframework.restdocs.payload.PayloadDocumentation์˜ ์ •์  ๋ฉ”์†Œ๋“œ๋กœ ์‘๋‹ต ํŽ˜์ด๋กœ๋“œ์˜ ํ•„๋“œ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์กฐ๊ฐ์„ ์ƒ์„ฑํ•˜๋Š” ์ฝ”๋“œ → ์š”์ฒญ์„ ๋ฌธ์„œํ™” ํ•˜๋ ค๋ฉด requestFields๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

(2) path๊ฐ€ ์žˆ๋Š” ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

→ ์œ„์— ์ฝ”๋“œ์˜ ๊ฒฐ๊ณผ๋กœ๋Š” ์‘๋‹ต ํ•„๋“œ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ํ…Œ์ด๋ธ”์ด ํฌํ•จ๋œ ์Šค๋‹ˆํŽซ์œผ๋กœ, ์‘๋‹ต์ด๋ฏ€๋กœ response-fields.adoc์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.

(์ด๋•Œ ๋ฌธ์„œํ™”๋˜์ง€ ์•Š์€ ํ•„๋“œ๋“ค์ด ์กด์žฌํ•  ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ ์‹คํŒจํ•˜๋Š”๋ฐ ๋ชจ๋“  ํ•„๋“œ๋ฅผ ๋ฌธ์„œ๋กœ ์ œ๊ณตํ•˜๊ณ  ์‹ถ์ง€ ์•Š์„ ๋•Œ๋Š” ํŽ˜์ด๋กœ๋“œ์˜ ์ „์ฒด ํ•˜์œ„ ์„น์…˜์„ ๋ฌธ์„œํ™”ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.)

 

  • ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜
this.mockMvc.perform(get("/users?page=2&per_page=100")) 
	.andExpect(status().isOk())
	.andDo(document("users", queryParameters( // (1)
			parameterWithName("page").description("The page to retrieve"), // (2)
			parameterWithName("per_page").description("Entries per page")
	)));

(1) ์š”์ฒญ์˜ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์กฐ๊ฐ์„ ์ƒ์„ฑํ•˜๋„๋ก Spring REST Docs ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. org.springframework.restdocs.request.RequestDocumentation. ์˜ queryParameters์˜ ์ •์  ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

(2) ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค์„ ๋ฌธ์„œํ™” ํ•  ๋•Œ๋Š” parameterWithName๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

 

  • ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜
this.mockMvc.perform(get("/locations/{latitude}/{longitude}", 51.5072, 0.1275)) 
	.andExpect(status().isOk())
	.andDo(document("locations", pathParameters( // (1) 
			parameterWithName("latitude").description("The location's latitude"),  // (2)
			parameterWithName("longitude").description("The location's longitude") 
	)));

(1) ์š”์ฒญ์˜ ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์„ค๋ช…ํ•˜๋Š” ์กฐ๊ฐ์„ ์ƒ์„ฑํ•˜๋„๋ก Spring REST Docs ๋ฅผ ๊ตฌ์„ฑํ•œ๋‹ค. ์ด๋•Œ org.springframework.restdocs.request.RequestDocumentation.์˜ pathParameters ์ •์  ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

(2) ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฌธ์„œํ™”ํ•  ๋•Œ๋Š”org.springframework.restdocs.request.RequestDocumentation.์˜ parameterWithName ์—์„œ ์ •์  ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

→ ์œ„์— ์ฝ”๋“œ์˜ ๊ฒฐ๊ณผ๋กœ๋Š” path parameters ํ•„๋“œ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ํ…Œ์ด๋ธ”์ด ํฌํ•จ๋œ ์Šค๋‹ˆํŽซ์œผ๋กœ, ์‘๋‹ต์ด๋ฏ€๋กœ path-parameters.adoc์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.

(์ด๋•Œ ๋ฌธ์„œํ™”๋˜์ง€ ์•Š์€ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋“ค์ด ์กด์žฌํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์‹คํŒจํ•˜๋Š”๋ฐ, ๋งŒ์•ฝ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฌธ์„œํ™”ํ•˜์ง€ ์•Š์œผ๋ ค๋ฉด ํ•ด๋‹น ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฌด์‹œ๋จ์œผ๋กœ ํ‘œ์‹œํ•  ์ˆ˜ ์žˆ๋‹ค. )

 

* ๋Œ€ํ‘œ์ ์ธ ๊ธฐ๋Šฅ๋“ค๋งŒ ์ •์˜ํ•œ ๊ฒƒ์œผ๋กœ, ๋” ์ž์„ธํ•œ ๊ธฐ๋Šฅ์€

https://docs.spring.io/spring-restdocs/docs/current/reference/htmlsingle/#introduction

→ Spring REST Docs ๊ณต์‹ ๋ฌธ์„œ ์ฐธ์กฐ

 

 

3๏ธโƒฃ  Spring REST Docs ๋ฅผ ์ด์šฉํ•œ Sample Project 

 

build.gradle 

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.2'
	id 'io.spring.dependency-management' version '1.1.4'

	id "org.asciidoctor.jvm.convert" version "3.3.2" // Asciidoctor plugin ์„ ์ ์šฉํ•ด์•ผ ํ•จ (sciiDoc ๋ฌธ๋ฒ•์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด Asciidoctor๋ฅผ ์ด์šฉํ•ด์„œ html๋กœ ๋ณ€ํ™˜ํ•ด์ค€๋‹ค.)
}

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

java {
	sourceCompatibility = '17'
}

configurations {
	asciidoctorExt // Asciidoctor ๋ฅผ ํ™•์žฅํ•˜๋Š” ์ข…์†์„ฑ์— ๋Œ€ํ•œ ๊ตฌ์„ฑ์„ ์„ ์–ธ
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'

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

	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}


ext {
	// snippetsDir ์ƒ์„ฑ๋œ ์กฐ๊ฐ(snipppets)์˜ ์ถœ๋ ฅ ์œ„์น˜๋ฅผ ์ •์˜ํ•˜๋Š” ์†์„ฑ์„ ๊ตฌ์„ฑ
	snippetsDir = file('build/generated-snippets')
}

test {
	// test ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด ์ถœ๋ ฅ์ด snippetsDir์— ๊ธฐ๋ก๋œ๋‹ค๋Š” ์ ์„ Gradle์ด ์ธ์‹ํ•˜๋„๋ก ํ•œ๋‹ค.
	outputs.dir snippetsDir
}

asciidoctor { // asciidoctor ์ž‘์—…์„ ๊ตฌ์„ฑ
	inputs.dir snippetsDir // 7. ์ž‘์—…์„ ์‹คํ–‰ํ•˜๋ฉด snippetDir์—์„œ ์ž…๋ ฅ์„ ์ฝ๊ฒŒ๋œ๋‹ค๊ณ  Gradle์— ์ธ์‹
	configurations 'asciidoctorExt'
	dependsOn test //  test ์ˆ˜ํ–‰ํ•œ ๋‹ค์Œ์— asciidoctor๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.
}

bootJar {
	dependsOn asciidoctor // test -> asciidoctor -> bootJar
	copy {
		from asciidoctor.outputDir
		into 'src/main/resources/static/docs'
	}
}

 

+spring.profiles.active = real (์‹ค์„œ๋ฒ„) ์ผ ๊ฒฝ์šฐ์— Spring REST Docs ๋ฌธ์„œ ์ƒ์„ฑ ๋ชปํ•˜๋„๋ก ๋ง‰๋Š” ๋ฐฉ๋ฒ•

bootJar {
	dependsOn asciidoctor // test -> asciidoctor -> bootJar

	String activeProfile = System.properties['spring.profiles.active']
	if(activeProfile != "real") {
		copy {
			from asciidoctor.outputDir
			into 'src/main/resources/static/docs'
		}
	}
}

-> ์‹ค์ œ ์šด์˜ ํ™˜๊ฒฝ์—์„œ api ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋˜๋ฉด ์•ˆ๋˜๊ธฐ ๋•Œ๋ฌธ์— bootJar ์‹คํ–‰ํ•  ๋•Œ ์ด๋ ‡๊ฒŒ ๋ง‰์•„๋‘๋ฉด ๋ฌธ์„œ ๋ณต์‚ฌ๋ฅผ ์•ˆํ•œ๋‹ค! 

 

 

*๋ฌธ์„œ์ž‘์„ฑ์„ ์œ„ํ•œ ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ (ํ”„๋กœ๋•์…˜ ์ฝ”๋“œ) 

: ๊ทธ๋ƒฅ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋งŒ ๋ณด์—ฌ์ค„๊นŒ.. ํ•˜๋‹ค๊ฐ€..๋ญ ์ฝ”๋“œ๊ฐ€ ๋งŽ์€ ๊ฒƒ๋„ ์•„๋‹ˆ๋‹ˆ..  ๊ทธ๋ƒฅ ์ฝ”๋“œ ๋‹ค ๊ณต๊ฐœํ•ฉ๋‹ˆ๋‹ค! (์ฐธ๊ณ ํ•˜์„ธ์œ ) 

 

Post

package com.sample.restDocs.vo;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor
public class Post
{
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String title;
	private String content;
	
	@Builder
	public Post(Long id, String title, String content)
	{
		this.id = id;
		this.title = title;
		this.content = content;
	}
	
	public void change(String title, String content)
	{
		this.title = title;
		this.content = content;
	}
}

-> ๊ฐ„๋‹จํ•˜๊ฒŒ ์ œ๋ชฉ, ๋‚ด์šฉ ์ž‘์„ฑ๋งŒ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค. 

 

 

PostController 

package com.sample.restDocs.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.controller.response.ApiResponse;
import com.sample.restDocs.service.response.PostResponse;
import com.sample.restDocs.service.PostService;

import lombok.RequiredArgsConstructor;

@RequestMapping("/api/v1")
@RequiredArgsConstructor
@RestController
public class PostController
{
	private final PostService postService;
	
	/**
	 * ๊ธ€ ์ž‘์„ฑ
	 * @param request
	 * @return
	 */
	@PostMapping("/post")
	public ApiResponse<PostResponse> writePost(@RequestBody PostCreateRequest request)
	{
		return ApiResponse.ok(postService.writePost(request));
	}
	
	/**
	 * ๊ธ€ ๋‹จ๊ฑด ์กฐํšŒ
	 * @param postId
	 * @return
	 */
	@GetMapping("/post/{postId}")
	public ApiResponse<PostResponse> getPost(@PathVariable(value = "postId") Long postId)
	{
		return ApiResponse.ok(postService.getPost(postId));
	}
	
	/**
	 * ๊ธ€ ์ˆ˜์ •
	 * @param postId
	 * @param request
	 * @return
	 */
	@PostMapping("/post/{postId}")
	public ApiResponse<PostResponse> updatePost(@PathVariable(value = "postId")Long postId, @RequestBody PostEditRequest request)
	{
		return ApiResponse.ok(postService.editPost(postId, request));
	}
	
}

-> ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๋กœ ๋งŒ๋“ค ๊ฑด ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ธ€ ์ž‘์„ฑ ๋ฐ ์ˆ˜์ •, ์กฐํšŒํ•˜๋Š” api ์ด๋‹ค. 

( ์ด์ •๋„๋Š” ๋ฌธ์„œ ๋งŒ๋“œ๋ ค๊ณ  ๊ธ€ ๋ณด๋Ÿฌ์˜ค์‹  ๋ถ„๋“ค์ด๋ผ๋ฉด ๋‹ค ์•Œ๊ฑฐ๋ผ.. ์ƒ๊ฐํ•˜๊ธฐ ๋•Œ๋ฌธ์— api ์ฝ”๋“œ์— ๋Œ€ํ•œ ์„ค๋ช…์€ ์ƒ๋ฝํ•˜๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค..  ํ˜น์‹œ ์งˆ๋ฌธ์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€ ๋‚จ๊ฒจ์ฃผ์„ธ์š” ๐Ÿ˜€)

 

PostCreateRequest 

package com.sample.restDocs.controller.request;

import com.sample.restDocs.vo.Post;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostCreateRequest
{
	private String title;
	
	private String content;
	
	@Builder
	public PostCreateRequest(String title, String content)
	{
		this.title = title;
		this.content = content;
	}
	
	public Post toEntity()
	{
		return Post.builder()
				.title(title)
				.content(content)
				.build();
	}
}

 

PostEditRequest 

package com.sample.restDocs.controller.request;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostEditRequest
{
	private String title;
	private String content;
	
	@Builder
	public PostEditRequest(String title, String content)
	{
		this.title = title;
		this.content = content;
	}
}

 

-> ์—ฌ๊ธฐ์„œ PostCreateRequest์™€ PostEditRequest ๋ฅผ ์™œ ๋ถ„๋ฆฌํ–ˆ๋Š”์ง€ ์ดํ•ด ์•ˆ๋  ์ˆ˜๋„ ์žˆ๋‹ค. 

์™œ๋ƒํ•˜๋ฉด ์ฝ”๋“œ๊ฐ€ ๊ฑฐ์˜ ๋˜‘๊ฐ™๊ธฐ ๋•Œ๋ฌธ !! 

์‚ฌ์‹ค ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๋ผ ๊ฐ™์•„ ๋ณด์ด๋Š” ๊ฒƒ์ด๋‹ค. ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๊ธ€ ์ž‘์„ฑ, ์ˆ˜์ • ๋ชจ๋‘ ์š”๊ตฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋Š”๋ฐ ์ด๊ฑธ ํ•˜๋‚˜๋กœ ํ†ต์ผํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฑด ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ๊ทธ๋ƒฅ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ๋”ฑ ์žˆ๋Š” dto๋ฅผ ๋งŒ๋“ค๊ณ  ์‹ถ์—ˆ๋‹ค. 

(๊ตณ์ด ์ƒ˜ํ”Œํ”„๋กœ์ ํŠธ์—์„œ ์ด๋ ‡๊ฒŒ ์•ˆ๋‚˜๋ˆ ๋„ ๋œ๋‹ค!! ์„ ํƒ ์‚ฌํ•ญ์ด๋‹ค.) 

(์šฐ๋ฆฌ xx์—์„  ํ•˜๋‚˜์˜ vo์— ๋‹ค ๋•Œ๋ ค๋„ฃ๋Š”๋ฐ... ๊ทธ๋Ÿฌ๋ฉด ํ•˜๋‚˜์˜ vo์— ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์ด ๋“ค์–ด๊ฐ€๊ณ ,,, ํ•œ vo์— ๊ฐ’์ด 20๊ฐœ๊ฐ€ ๋˜๊ธฐ๋„ ํ•œ๋‹ค... ์™œ ์ด๋ ‡๊ฒŒ ํ•˜๋Š”์ง€ ๋„์ €ํžˆ ๋ชจ๋ฅด๊ฒ ์ง€๋งŒ.. dto ๋งŒ๋“ค์—ˆ๋‹ค๊ฐ€ ์‚ญ์ œํ•˜๋ผ๊ณ  ํ•œ ์†Œ๋ฆฌ๋“ค์–ด์„œ... ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ์—์„œ๋งŒํผ์ด๋ผ๋„ ๋‚ด ๋ง˜๋Œ€๋กœ ํ•œ๋‹ค!!!! ใ…‹ใ…‹ ์‚ฌ์กฑ์ž„.. ๋„˜์–ด๊ฐ€์„ธ์š” ) 

 

ApiResponse

package com.sample.restDocs.controller.response;

import org.springframework.http.HttpStatus;

import lombok.Getter;

@Getter
public class ApiResponse<T>
{
	private int code;
	
	private HttpStatus status;
	private String message;
	
	private T data;
	
	public ApiResponse(HttpStatus status, String message, T data)
	{
		this.code = status.value();
		this.status = status;
		this.message = message;
		this.data = data;
	}
	
	public static <T> ApiResponse<T> of(HttpStatus status, String message, T data)
	{
		return new ApiResponse<>(status, message, data);
	}
	
	public static <T> ApiResponse<T> of(HttpStatus status, T data)
	{
		return new ApiResponse<>(status, status.name(), data);
	}
	
	public static <T> ApiResponse<T> ok(T data)
	{
		return of(HttpStatus.OK, data);
	}
}

-> ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๊ทธ๋ƒฅ vo ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ๋„ ๋˜๊ฒ ์ง€๋งŒ.. ๋ณดํ†ต์€ ํด๋ผ์ด์–ธํŠธ์™€ ํ˜‘์˜ํ•ด์„œ ์–ด๋–ค ํ˜•์‹์œผ๋กœ ์ค„ ์ง€ ์ •ํ•œ๋‹ค. 

๊ทธ๋Ÿฌ๋ฏ€๋กœ code, message, status, data ์ •๋„๋กœ ํ•ด์„œ ApiResponse๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค. 

 

 

PostService 

package com.sample.restDocs.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.service.response.PostResponse;
import com.sample.restDocs.vo.Post;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class PostService
{
	private final PostRepository postRepository;
	
	public PostResponse getPost(Long postId)
	{
		Post post = postRepository.findById(postId).orElseThrow(() -> new RuntimeException("ํ•ด๋‹น Post๋Š” ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
		
		return PostResponse.builder().title(post.getTitle())
				.content(post.getContent()).build();
	}
	
	public PostResponse writePost(PostCreateRequest request)
	{
		Post requestPost = request.toEntity();
		
		Post save = postRepository.save(requestPost);
		
		return PostResponse.builder().title(save.getTitle())
				.content(save.getContent()).build();
	}
	
	@Transactional
	public PostResponse editPost(Long postId, PostEditRequest request)
	{
		Post post = postRepository.findById(postId).orElseThrow(() -> new RuntimeException("ํ•ด๋‹น Post๋Š” ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."));
		
		post.change(request.getTitle(), request.getContent());
		
		return PostResponse.builder().title(post.getTitle())
				.content(post.getContent()).build();
	}
}

 

PostResponse

package com.sample.restDocs.service.response;

import lombok.Builder;
import lombok.Getter;

@Getter
public class PostResponse
{
	private String title;
	private String content;
	
	@Builder
	public PostResponse(String title, String content)
	{
		this.title = title;
		this.content = content;
	}
}

-> ์—ฌ๊ธฐ์„œ ๋˜ ์˜์•„ํ•œ ? ์‚ฌ๋žŒ๋“ค์ด ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค. 

์™œ ์„œ๋น„์Šค ๊นŒ์ง€ ์ด๋ ‡๊ฒŒ ๋งŒ๋“œ๋ƒ!! ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•˜์ง€๋งŒ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ๋Š” ๊ฐ’ ์ฒดํฌ๋„ ํ•ด์•ผํ•˜๊ณ , ์ด๋Ÿฐ ๋กœ์ง์ด ๋“ค์–ด๊ฐˆ ๊ฑด๋ฐ, ์„œ๋น„์Šค์—์„œ๋„ ์ด ๊ฐ์ฒด๋ฅผ ๊ฐ€์ ธ์™€ ์‚ฌ์šฉํ•˜๊ธฐ๋ณด๋‹ค๋Š” ์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค๋Š” ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ๋” ๋‚ซ๋‹ค~ ๋ผ๋Š” ๊ฐ•์˜๋ฅผ ๋“ค์—ˆ๋‹ค. 

์ปจํŠธ๋กค๋Ÿฌ ์ •์ฑ…์„ ์„œ๋น„์Šค๊นŒ์ง€ ๊ฐ€์ ธ์˜ค์ง€ ๋งˆ๋ผ~ ๋ญ ์ด๋Ÿฐ ๊ฒƒ ๊ฐ™๋‹ค.

(์ด๊ฒƒ๋„ ์‚ฌ์‹ค ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ์—์„œ๊นŒ์ง€ ํ•  ํ•„์š”๋Š” ์—†์ง€๋งŒ ๋‚˜๋Š” ์Šต๊ด€ ๋“ค์ด๊ณ  ์‹ถ์—ˆ๋‹ค.) 

 

PostRepository 

package com.sample.restDocs.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.sample.restDocs.vo.Post;

@Repository
public interface PostRepository extends JpaRepository<Post, Long>
{
}

 

 

*๋ฌธ์„œ์ž‘์„ฑ ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ (ํ…Œ์ŠคํŠธ ์ฝ”๋“œ) 

PostControllerTest 

package com.sample.restDocs.controller;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.service.PostService;
import com.sample.restDocs.vo.Post;

@AutoConfigureMockMvc // @SpringBootTest ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ…Œ์ŠคํŠธ์—์„œ MockMvc๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์— ์‚ฌ์šฉ
@SpringBootTest
class PostControllerTest
{
	@Autowired
	private MockMvc mockMvc;
	
	@Autowired
	private ObjectMapper objectMapper;
	
	@Autowired
	private PostService postService;
	
	@Autowired
	private PostRepository postRepository;
	
	@BeforeEach
	void clean() {
		postRepository.deleteAll(); // ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ ํ›„ ๋ฐ์ดํ„ฐ ํด๋ Œ์ง•
	}
	
	@DisplayName("๊ธ€ ์ž‘์„ฑ ํ…Œ์ŠคํŠธ")
	@Test
	void write() throws Exception
	{
	    // given
		PostCreateRequest request = PostCreateRequest.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ")
				.content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
		
		// when
		ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/post")
													   .contentType(MediaType.APPLICATION_JSON)
													   .content(objectMapper.writeValueAsString(request)))
										.andDo(MockMvcResultHandlers.print());
		
		// then
		result.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ"));
	}
	
	
	@DisplayName("๊ธ€ 1๊ฐœ๋ฅผ ์ €์žฅ ํ›„ ํ•ด๋‹น ๊ธ€ ์กฐํšŒ ํ…Œ์ŠคํŠธ")
	@Test
	void get() throws Exception
	{
	    // given
		Post post = Post.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
		Post response = postRepository.save(post);
		
		// when
		ResultActions result = mockMvc.perform(MockMvcRequestBuilders.get("/api/v1/post/{postId}", response.getId()))
				.andDo(MockMvcResultHandlers.print());
		
		// then
		result.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ"));
	}
	
	
	@DisplayName("๊ธ€ 1๊ฐœ๋ฅผ ์ˆ˜์ • ํ›„ ํ•ด๋‹น ๊ธ€ ์กฐํšŒ ํ…Œ์ŠคํŠธ")
	@Test
	void update() throws Exception
	{
	    // given
		Post post = Post.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
		Post response = postRepository.save(post);

		PostEditRequest editRequest = PostEditRequest.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ ์ˆ˜์ •!").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ ์ˆ˜์ •!").build();
		
		// when
		ResultActions result = mockMvc.perform(MockMvcRequestBuilders.post("/api/v1/post/{postId}", response.getId())
								.contentType(MediaType.APPLICATION_JSON)
								.content(objectMapper.writeValueAsString(editRequest)))
				.andDo(MockMvcResultHandlers.print());
		
		// then
		result.andExpect(MockMvcResultMatchers.status().isOk())
				.andExpect(MockMvcResultMatchers.jsonPath("$.code").value("200"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.title").value("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ ์ˆ˜์ •!"))
				.andExpect(MockMvcResultMatchers.jsonPath("$.data.content").value("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ ์ˆ˜์ •!"));
	}
	
}

 

-> ์ด๊ฑด ๊ทธ๋ƒฅ PostController๋ฅผ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. (์ฐธ๊ณ ..ใ…Ž) 

์‚ฌ์‹ค ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฒ•์„ ์ด์ œ ๋ง‰ ๋ฐฐ์›Œ์„œ ๊ณต๋ถ€ํ•˜๊ณ  ํ•˜๋Š” ๋‹จ๊ณ„๋ผ.. ํ…Œ์ŠคํŠธ ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์™„๋ฒฝํ•˜๋‹ค๊ณ  ํ•  ์ˆ˜๋Š” ์—†์ง€๋งŒ ์ผ๋‹จ ์ €๋Š” ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ๋กœ ๋งŒ๋“  api ํ…Œ์ŠคํŠธ๋ฅผ ์ด๋ ‡๊ฒŒ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. 

(๊ทธ๋ฆฌ๊ณ  Service ๋„ ๋”ฐ๋กœ ํ…Œ์ŠคํŠธํ•˜๋Š”๊ฒŒ ๋งž๋Š”๋ฐ.. ์‹œ๊ฐ„๊ด€๊ณ„์ƒ ์ €๋Š” ์ƒ๋žตํ–ˆ์ง€๋งŒ..๋งŒ๋“ค์–ด๋ณด์‹œ๊ธธ... Service๋‹จ ํ…Œ์ŠคํŠธ๋„ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค!!) 

 

 

RestDocsSupport

package com.sample.restDocs.docs;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.restdocs.RestDocumentationContextProvider;
import org.springframework.restdocs.RestDocumentationExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.fasterxml.jackson.databind.ObjectMapper;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration;

/**
 * Spring REST Docs ํ™˜๊ฒฝ ํด๋ž˜์Šค
 */
@AutoConfigureRestDocs( // Spring REST Docs ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด MockMvc ๋นˆ ์ปค์Šคํ…€
		uriScheme = "https", uriHost = "sample.restDocs.com"
)
@SpringBootTest
@ExtendWith(RestDocumentationExtension.class)
public class RestDocsSupport
{
	protected MockMvc mockMvc;
	
	@Autowired
	protected ObjectMapper objectMapper;
	
	@BeforeEach
	void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation)
	{
		// @AutoConfigureMockMvc ๋ฅผ ํ†ตํ•œ ์ž๋™์ฃผ์ž…์ด ์•„๋‹ˆ๋ผ ํ…Œ์ŠคํŠธ ์ „์— ๋ฏธ๋ฆฌ mockMvc ์„ธํŒ…ํ•˜๋Š” ์ž‘์—…
		this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
				.apply(documentationConfiguration(restDocumentation))
				.build();
	}
	
	
}

-> Rest Docs ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ํ™˜๊ฒฝ ์„ค์ •? ์ด๋ผ๊ณ  ํ•  ์ˆ˜ ์žˆ๋‹ค. 

์šฐ์„  @ExtendWith(RestDocumentationExtension.class)๋ฅผ ์ถ”๊ฐ€ํ•ด์ค˜์•ผ ํ•œ๋‹ค. 

๊ทธ๋ฆฌ๊ณ  @AutoConfigureRestDocs๋Š” Spring REST Docs ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด MockMvc ๋นˆ ์ปค์Šคํ…€ ํ•˜๋Š” ๊ฑฐ๋ผ๊ณ  ํ•œ๋‹ค..! 

 

-> @BeforeEach๋ฅผ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰ ์ „์— MockMvcRestDocumentationConfigurer. documentationConfiguration()์˜ ์ •์  ๋ฉ”์„œ๋“œ ์—์„œ ์ด ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์–ป๋Š”๋‹ค.

 

 

PostControllerDocsTest 

package com.sample.restDocs.docs;

import java.util.List;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders;
import org.springframework.restdocs.payload.FieldDescriptor;
import org.springframework.restdocs.payload.JsonFieldType;
import org.springframework.restdocs.payload.PayloadDocumentation;
import org.springframework.restdocs.request.RequestDocumentation;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
import com.epages.restdocs.apispec.ResourceDocumentation;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import com.epages.restdocs.apispec.Schema;
import com.sample.restDocs.controller.request.PostCreateRequest;
import com.sample.restDocs.controller.request.PostEditRequest;
import com.sample.restDocs.repository.PostRepository;
import com.sample.restDocs.vo.Post;

public class PostControllerDocsTest extends RestDocsSupport
{
    @Autowired
    private PostRepository postRepository;
    
    protected List<FieldDescriptor> defaultResponseFieldDescriptors = new java.util.ArrayList<>(
          List.of(
                PayloadDocumentation.fieldWithPath("code").type(JsonFieldType.NUMBER).description("์‘๋‹ต ์ฝ”๋“œ"),
                PayloadDocumentation.fieldWithPath("status").type(JsonFieldType.STRING).description("์‘๋‹ต ์ƒํƒœ"),
                PayloadDocumentation.fieldWithPath("message").type(JsonFieldType.STRING).description("์‘๋‹ต ๋ฉ”์‹œ์ง€"),
                PayloadDocumentation.fieldWithPath("data").type(JsonFieldType.OBJECT).description("์‘๋‹ต ๋ฐ์ดํ„ฐ")
          ));
    
    @DisplayName("๊ธ€ ํ•˜๋‚˜๋ฅผ ์ž‘์„ฑํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ์ €์žฅ๋œ๋‹ค.")
    @Test
    void write() throws Exception
    {
       // given
       PostCreateRequest request = PostCreateRequest.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ")
             .content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
       
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ œ๋ชฉ"));
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋‚ด์šฉ"));
       
       // when
       ResultActions result = mockMvc.perform(RestDocumentationRequestBuilders.post("/api/v1/post")
                   .contentType(MediaType.APPLICATION_JSON)
                   .content(objectMapper.writeValueAsString(request)))
                   .andDo(MockMvcResultHandlers.print());

       // then
       result.andExpect(MockMvcResultMatchers.status().isOk())
             .andDo(MockMvcRestDocumentation.document("post-write", // REST Docs
                                            RestDocsUtils.getDocumentRequest(),
                                            RestDocsUtils.getDocumentResponse(),
                                            PayloadDocumentation.requestFields( // ์š”์ฒญ ๋ฐ์ดํ„ฐ ์ŠคํŽ™
                                                 PayloadDocumentation.fieldWithPath("title").description("post ์ œ๋ชฉ").optional(),
                                                 PayloadDocumentation.fieldWithPath("content").description("post ๋‚ด์šฉ")
                                            ),
                                            PayloadDocumentation.responseFields( // ์‘๋‹ต ๋ฐ์ดํ„ฐ ์ŠคํŽ™
                                                  defaultResponseFieldDescriptors
                                            )
             ));
       ;
    }
    
    @DisplayName("๊ธ€ 1๊ฐœ๋ฅผ ์ €์žฅํ•˜๊ณ  ์กฐํšŒํ•˜๋Š” ํ…Œ์ŠคํŠธ")
    @Test
    void get() throws Exception
    {
       // given
       Post post = Post.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
       Post response = postRepository.save(post);
       
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ œ๋ชฉ"));
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋‚ด์šฉ"));
       
       // when
       ResultActions result = mockMvc.perform(RestDocumentationRequestBuilders.get("/api/v1/post/{postId}", response.getId()))
                               .andDo(MockMvcResultHandlers.print());
       
       // then
       result.andExpect(MockMvcResultMatchers.status().isOk())
             .andDo(MockMvcRestDocumentation.document("post-get", // REST docs
                                            RestDocsUtils.getDocumentResponse(),
                                            RequestDocumentation.pathParameters(
                                                  RequestDocumentation.parameterWithName("postId").description("post Id")
                                            ),
                                            PayloadDocumentation.responseFields(
                                                  defaultResponseFieldDescriptors
                                            )
             ));
       
    }
    
    @DisplayName("๊ธ€ 1๊ฐœ๋ฅผ ์ˆ˜์ • ํ›„ ํ•ด๋‹น ๊ธ€ ์กฐํšŒ ํ…Œ์ŠคํŠธ")
    @Test
    void update() throws Exception
    {
       // given
       Post post = Post.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ").build();
       Post response = postRepository.save(post);
       
       PostEditRequest editRequest = PostEditRequest.builder().title("ํ…Œ์ŠคํŠธ ์ œ๋ชฉ ์ˆ˜์ •!").content("ํ…Œ์ŠคํŠธ ๋‚ด์šฉ ์ˆ˜์ •!").build();
       
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.title").description("post ์ œ๋ชฉ"));
       defaultResponseFieldDescriptors.add(PayloadDocumentation.fieldWithPath("data.content").description("post ๋‚ด์šฉ"));
       
       // when
       ResultActions result = mockMvc.perform(
                   RestDocumentationRequestBuilders.post("/api/v1/post/{postId}", response.getId())
                         .contentType(MediaType.APPLICATION_JSON)
                         .content(objectMapper.writeValueAsString(editRequest)))
             .andDo(MockMvcResultHandlers.print());
       
       // then
       result.andExpect(MockMvcResultMatchers.status().isOk())
             .andDo(MockMvcRestDocumentation.document("post-update", // REST Docs
                                            RestDocsUtils.getDocumentRequest(),
                                            RestDocsUtils.getDocumentResponse(),
                                            RequestDocumentation.pathParameters(
                                                  RequestDocumentation.parameterWithName("postId").description("post Id")
                                            ),
                                            PayloadDocumentation.requestFields(
                                              PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING).description("post ์ œ๋ชฉ").optional(),
                                              PayloadDocumentation.fieldWithPath("content").type(JsonFieldType.STRING).description("post ๋‚ด์šฉ")
                                            ),
                                            PayloadDocumentation.responseFields(
                                                  defaultResponseFieldDescriptors
                                            )
                                            ));
    }
}

-> ์ด๋ ‡๊ฒŒ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์™€ ๋ฌธ์„œ ์ž‘์„ฑ ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค. 

 

๊ทธ๋ฆฌ๊ณ  bootJar ์‹คํ–‰ํ•˜๋ฉด 

์Šค๋‹ˆํŽซ์ด ์ƒ์„ฑ๋œ๋‹ค. 

-> ๊ทธ๋ฆฌ๊ณ  src/main/resources/static/docs๋กœ index.html ๋กœ ๋ณ€ํ™˜๋˜์–ด copy ๋œ ๊ฒƒ๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

 

์Šค๋‹ˆํŽซ ์‚ฌ์šฉ ์ฝ”๋“œ

index.adoc

= RestDocs Test API
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 2
:sectlinks:

== ๊ธ€ ์ž‘์„ฑ

=== ์š”์ฒญ
include::{snippets}/post-write/http-request.adoc[]

=== ์‘๋‹ต
include::{snippets}/post-write/http-response.adoc[]

include::{snippets}/post-write/response-fields.adoc[]

== ๊ธ€ ๋‹จ๊ฑด ์กฐํšŒ

=== ์š”์ฒญ
include::{snippets}/post-get/http-request.adoc[]

include::{snippets}/post-get/path-parameters.adoc[]

=== ์‘๋‹ต

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

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

-> ์ƒ์„ฑ๋œ ์Šค๋‹ˆํŽซ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์ „์— ์†Œ์Šค ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด์•ผ ํ•œ๋‹ค. 

src/docs/asciidoc์— index.adoc ์„ ๋งŒ๋“ค๊ณ  ์ด๋Ÿฐ์‹์œผ๋กœ ์Šค๋‹ˆํŽซ๋“ค์„ ์กฐํ•ฉํ•ด์„œ ๋ฌธ์„œ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋‹ค. 

adoc ๋ฌธ์„œ๋Š” ์ปค์Šคํ…€ ๊ฐ€๋Šฅํ•˜๊ณ , adoc ๋ฌธ๋ฒ•์„ ๊ณต๋ถ€ํ•ด์„œ ์ ์šฉํ•˜๋ฉด ๋œ๋‹ค. 

https://docs.asciidoctor.org/ → asciidoctor ๊ณต์‹ ๋ฌธ์„œ

 

 

๊ฒฐ๊ณผ ํ™”๋ฉด 

์ง€๊ธˆ์€ ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ์ด๋ฏ€๋กœ http://localhost:8080/docs/index.html ์—์„œ ํŒŒ์ผ api ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

 

 

๐Ÿ˜Ž ๊ฐ„๋‹จํ•˜๊ฒŒ Spring REST Docs ๋ฅผ ์ด์šฉํ•ด์„œ api ๋ฌธ์„œ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ดค๋‹ค.

๊ณต์‹ ๋ฌธ์„œ๋งŒ ๋ณผ ๋•Œ ๊นŒ์ง€๋Š” ??? ํ•˜๋Š” ์ดํ•ด ์•ˆ๋˜๋Š” ๋ถ€๋ถ„๋“ค์ด ์žˆ์—ˆ๋Š”๋ฐ ์—ญ์‹œ ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๊ณ , ์˜ค๋ฅ˜ ๋‚˜๋ฉด ์ฐพ์•„๋„ ๋ณด๊ณ  ํ•˜๋ฉด์„œ ์ดํ•ดํ•˜๊ณ  ๊ฒฐ๊ณผ ํ™”๋ฉด์„ ๋ณด๋‹ˆ๊นŒ ์ดํ•ด๊ฐ€ ๋” ์ž˜ ๋œ ๊ฒƒ ๊ฐ™๋‹ค. 

์‚ฌ์‹ค Spring REST Docs ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์–ผ๋งˆ๋‚˜ ๊ณต๋“ค์—ฌ์„œ? ์ž‘์„ฑํ•˜๋А๋ƒ์— ๋”ฐ๋ผ์„œ ํ€„๋ฆฌํ‹ฐ๋Š” ๋‹ฌ๋ผ์งˆ ๊ฒƒ ๊ฐ™๋‹ค.

ํ•˜์ง€๋งŒ ๋ญ.. ์ด์ •๋„๋งŒ ํ•ด๋„ ๊น”๋”ํ•˜๋‹ˆ ๋ฌธ์„œ๋ณด๋Š”๋ฐ ์–ด๋ ต์ง€ ์•Š๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋Š”๋ฐ... ์ด๊ฑด ๋‚ด ์ƒ๊ฐ์ผ ๋ฟ.. ใ…‹ 

์ด๊ฑด ์ง„์งœ ์ดˆ๊ฐ„๋‹จ์œผ๋กœ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•์„ ์ตํžˆ๊ธฐ ์œ„ํ•ด ๋งŒ๋“  ํ”„๋กœ์ ํŠธ์ด๋ฏ€๋กœ ์‹ค์ „์—์„œ๋Š” ๋” ์‹ ๊ฒฝ์จ์„œ ํ…Œ์ŠคํŠธ๋„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•˜๊ณ , ๋ฌธ์„œ๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค. 

 

Spring REST Docs๋ฅผ ์‹ค๋ฌด์—์„œ ์ด์šฉํ•œ๋‹ค๋ฉด ๋จผ์ € api ๋ฌธ์„œ๊ฐ€ ํ•„์š”ํ•  ํ…๋ฐ ๊ทธ๋Ÿด๋•Œ ์–ด๋–ป๊ฒŒ service.. ๋“ฑ๋“ฑ์„ ์–ธ์ œ ๋งŒ๋“ค์–ด์„œ ๋ฌธ์„œ ๊ณต์œ ํ•˜๊ฒ ๋Š”๊ฐ€..! 

์šฐ์„  tdd๋กœ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๊ณ  ControllerTest๋งŒ Mock์ฒ˜๋ฆฌ?ํ•ด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑํ›„ Spring rest doc์œผ๋กœ ๋น ๋ฅด๊ฒŒ ๋ฌธ์„œ๋งŒ ์ž‘์„ฑํ•ด์„œ ํด๋ผ์ด์–ธํŠธ์™€ ๊ณต์œ ํ•˜๊ณ , ๊ทธ ๋’ค์— ๋น„์ง€๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์ด์šฉํ•˜๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค. 

 

728x90