chap 11 - JAVA-JIKIMI/SPRING-IN-ACTION-5 GitHub Wiki

11. ๋ฆฌ์•กํ‹ฐ๋ธŒ API ๊ฐœ๋ฐœํ•˜๊ธฐ

์Šคํ”„๋ง WebFlux๋Š” ์Šคํ”„๋ง MVC์˜ ๋Œ€์•ˆ์ด๋ฉฐ ์Šคํ”„๋ง MVC์™€ ๋™์ผํ•œ ๊ฐœ๋ฐœ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ด์–ด ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ์„ ํƒ์˜ ๊ธฐํšŒ๋ฅผ ์ œ๊ณต

์Šคํ”„๋ง 5์˜ ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ฐฉ์‹์„์‚ฌ์šฉํ•ด์„œ ๋ฆฌ์•กํ‹ฐ๋ธŒ API๋ฅผ ์ƒ์„ฑ

https://devahea.github.io/2019/04/21/Spring-WebFlux๋Š”-์–ด๋–ป๊ฒŒ-์ ์€-๋ฆฌ์†Œ์Šค๋กœ-๋งŽ์€-ํŠธ๋ž˜ํ”ฝ์„-๊ฐ๋‹นํ• ๊นŒ/

https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html

๋™์ž‘ ํ๋ฆ„ https://m.blog.naver.com/PostView.nhn?blogId=gngh0101&logNo=221538537388&proxyReferer=https:%2F%2Fwww.google.com%2F

์ด ์žฅ์—์„œ ๋ฐฐ์šฐ๋Š” ๋‚ด์šฉ

  • ์Šคํ”„๋ง WebFlux ์‚ฌ์šฉํ•˜๊ธฐ
  • ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํŠธ๋กค๋Ÿฌ์™€ ํด๋ผ์ด์–ธํŠธ ์ž‘์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธํ•˜๊ธฐ
  • REST API ์†Œ๋น„ํ•˜๊ธฐ
  • ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ณด์•ˆ

๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ๊ณผ ํ”„๋กœ์ ํŠธ ๋ฆฌ์•กํ„ฐ์˜ ํ•ต์‹ฌ ๋‚ด์šฉ์„ 10์žฅ์—์„œ ์•Œ์•„๋ณด์•˜๊ณ  ์ด๋Ÿฐ ๊ธฐ๋ฒ•์„ ์Šคํ”„๋ง ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ ์šฉ

6์žฅ์—์„œ ์ž‘์„ฑํ–ˆ๋˜ ์ปจํŠธ๋กค๋Ÿฌ์— ์Šคํ”„๋ง 5์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์„ ์‚ฌ์šฉ

์Šคํ”„๋ง 5์˜ ์ƒˆ๋กœ์šด ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ธ ์Šคํ”„๋ง WebFlux

  • ์Šคํ”„๋ง WebFlux๋Š” ์Šคํ”„๋ง MVC์™€ ๋งค์šฐ ์œ ์‚ฌํ•˜๋ฉฐ ์ ์šฉํ•˜๊ธฐ ์‰ฝ๋‹ค. ์Šคํ”„๋ง REST API ์ƒ์„ฑ์— ๊ด€ํ•ด ์šฐ๋ฆฌ๊ฐ€ ์ด๋ฏธ ์•Œ๊ณ  ์žˆ๋Š” ๊ฒƒ์˜ ๋งŽ์€ ๋ถ€๋ถ„์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

11.1 ์Šคํ”„๋ง WebFlux ์‚ฌ์šฉํ•˜๊ธฐ

  • ๋งค ์—ฐ๊ฒฐ๋งˆ๋‹ค ํ•˜๋‚˜์˜ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์Šคํ”„๋ง MVC ๊ฐ™์€์ „ํ˜•์ ์ธ ์„œ๋ธ”๋ฆฟ ๊ธฐ๋ฐ˜์˜ ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์Šค๋ ˆ๋“œ ๋ธ”๋กœํ‚น(์ฐจ๋‹จ)๊ณผ ๋‹ค์ค‘ ์Šค๋ ˆ๋“œ๋กœ ์ˆ˜ํ–‰๋œ๋‹ค.
  • ๋”ฐ๋ผ์„œ ๋ธ”๋กœํ‚น ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ์š”์ฒญ๋Ÿ‰์˜ ์ฆ๊ฐ€์— ๋”ฐ๋ฅธ ํ™•์žฅ์ด ์‚ฌ์‹ค์ƒ ์–ด๋ ต๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ์ฒ˜๋ฆฌ๊ฐ€ ๋А๋ฆฐ ์ž‘์—… ์Šค๋ ˆ๋“œ๋กœ ์ธํ•ด ํ›จ์”ฌ ๋” ์‹ฌ๊ฐํ•œ ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•œ๋‹ค.
  • ํ•ด๋‹น ์ž‘์—… ์Šค๋ ˆ๋“œ๊ฐ€ ํ’€๋กœ ๋ฐ˜ํ™˜๋˜์–ด ๋‹ค๋ฅธ ์š”์ฒญ ์ฒ˜๋ฆฌ๋ฅผ ์ค€๋น„ํ•˜๋Š” ๋ฐ ๋” ๋งŽ์€ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ๋Š” ์ด ๋ฐฉ์‹์ด ์™„๋ฒฝํžˆ ๋ฐ›์•„๋“ค์ผ ๋งŒํ•˜๋‹ค.
  • ์‹ค์ œ๋กœ ์ด๊ฒƒ์€ ๋Œ€๋ถ€๋ถ„์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์— 10๋…„ ๋„˜๊ฒŒ ์‚ฌ์šฉ๋œ ๋ฐฉ๋ฒ•์ด๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์‹œ๋Œ€๊ฐ€ ๋ฐ”๋€Œ๊ณ  ์žˆ๋‹ค.

๋น„๋™๊ธฐ ์›น ํ”„๋ ˆ์ž„์›Œํฌ

  • ๋” ์ ์€ ์ˆ˜์˜ ์Šค๋ ˆ๋“œ(์ผ๋ฐ˜์ ์œผ๋กœ CPU ์ฝ”์–ด๋‹น ํ•˜๋‚˜)๋กœ ๋” ๋†’์€ ํ™•์žฅ์„ฑ์„ ์„ฑ์ทจํ•œ๋‹ค.
  • ์ด๋ฒคํŠธ ๋ฃจํ•‘(event loop) ์ด๋ผ๋Š” ๊ธฐ๋ฒ•์„ ์ ์šฉํ•œ ์ด๋Ÿฐ ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ํ•œ ์Šค๋ ˆ๋“œ๋‹น ๋งŽ์€ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์„œ ํ•œ ์—ฐ๊ฒฐ๋‹น ์†Œ์š” ๋น„์šฉ์ด ๋” ๊ฒฝ์ œ์ ์ด๋‹ค.
  • ๋น„์šฉ์ด ๋“œ๋Š” ์ž‘์—…์ด ํ•„์š”ํ•  ๋•Œ ์ด๋ฒคํŠธ ๋ฃจํ”„๋Š” ํ•ด๋‹น ์ž‘์—…์˜ ์ฝœ๋ฐฑ(call back)์„ ๋“ฑ๋กํ•˜์—ฌ ๋ณ‘ํ–‰์œผ๋กœ ์ˆ˜ํ–‰๋˜๊ฒŒ ํ•˜๊ณ  ๋‹ค๋ฅธ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋กœ ๋„˜์–ด๊ฐ„๋‹ค.

11.1.1 ์Šคํ”„๋ง WebFlux ๊ฐœ์š”

  • ์Šคํ”„๋ง 5๋Š” WebFlux๋ผ๋Š” ์ƒˆ๋กœ์šด ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ Flux๋Š” ์Šคํ”„๋ง MVC์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ณต์œ ํ•œ๋‹ค.
  • Spring MVC๋Š” ์ž๋ฐ” ์„œ๋ธ”๋ฆฟ API์˜ ์ƒ์œ„ ๊ณ„์ธต์— ์œ„์น˜

WebFlux ํŠน์ง•

  • Spring WebFlux ๋Š” ์„œ๋ธ”๋ฆฟ API์™€ ์—ฐ๊ณ„๋˜์ง€ ์•Š๋Š”๋‹ค. ์„œ๋ธ”๋ฆฟ API๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋ฒ„์ „์ธ ๋ฆฌ์•กํ‹ฐ๋ธŒ HTTP API์˜ ์ƒ์œ„ ๊ณ„์ธต์— ์œ„์น˜ํ•œ๋‹ค.
  • Spring WebFlux๋Š” ์„œ๋ธ”๋ฆฟ API์— ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์‹คํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ธ”๋ฆฟ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ•„์š”๋กœ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋Œ€์‹ ์— ๋ธ”๋กœํ‚น์ด ์—†๋Š” ์–ด๋–ค ์›น ์ปจํ…Œ์ด๋„ˆ์—์„œ๋„ ์‹คํ–‰๋  ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด์—๋Š” Netty, Undertow, ํ†ฐ์บฃ, Jetty ๋˜๋Š” ๋‹ค๋ฅธ ์„œ๋ธ”๋ฆฟ 3.1 ์ด์ƒ์˜ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ํฌํ•จ๋œ๋‹ค.
  • ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ •์˜ํ•˜๋Š” ๋Œ€์•ˆ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค.

11.1.2 ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํŠธ๋กค๋Ÿฌ ์ž‘์„ฑํ•˜๊ธฐ

  • 6์žฅ์—์„œ ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ์˜ REST API ์ปจํŠธ๋กค๋Ÿฌ ์ƒ์„ฑ์„ ๋ฆฌ์•กํ‹ฐ๋ธŒ๋ฅผ ๋ณ€๊ฒฝ
  • ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ปจํŠธ๋กค๋Ÿฌ - ๋„๋ฉ”์ธ ํƒ€์ž…(Taco, Order) ๋˜๋Š” ๋„๋ฉ”์ธ ํƒ€์ž…์˜ ์ปฌ๋ ‰์…˜์œผ๋กœ ์ž…๋ ฅ๊ณผ ์ถœ๋ ฅ์„ ์ฒ˜๋ฆฌ
  • Iterable์€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์ด ์•„๋‹ˆ๋ฉฐ Iterable์—๋Š” ์–ด๋–ค ๋ฆฌ์•กํ‹ฐ๋ธŒ ์˜คํผ๋ ˆ์ด์…˜๋„ ์ ์šฉํ•  ์ˆ˜ ์—†์Œ
  • ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ Iterable ํƒ€์ž…์„ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ์— ๊ฑธ์ณ ์ž‘์—…์„ ๋ถ„ํ• ํ•˜๊ฒŒ ํ•  ์ˆ˜๋„ ์—†๋‹ค.
  • recentTacos()๊ฐ€ Flux ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

RxJava ํƒ€์ž… ์‚ฌ์šฉํ•˜๊ธฐ

  • ์Šคํ”„๋ง WebFlux๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ Flux๋‚˜ Mono์™€ ๊ฐ™์€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์ด ์ž์—ฐ์Šค๋Ÿฌ์šด ์„ ํƒ์ด์ง€๋งŒ Observable์ด๋‚˜ Single๊ณผ ๊ฐ™์€ RxJava ํƒ€์ž…์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • WebFlux๋Š” ๋˜ํ•œ Observable์ด๋‚˜ ๋ฆฌ์•กํ„ฐ Flux ํƒ€์ž…์˜ ๋Œ€์•ˆ์œผ๋กœ Flowable ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜๊ฒŒ ์ž…๋ ฅ ์ฒ˜๋ฆฌํ•˜๊ธฐ

  • ์ง€๊ธˆ๊นŒ์ง€๋Š” ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์„œ๋“œ๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…
  • ์Šคํ”„๋ง WebFlux๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ ๋ฉ”์„œ๋“œ์˜ ์ž…๋ ฅ์œผ๋กœ๋„ Mono๋‚˜ Flux๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
  • ๋ฆฌํผ์ง€ํ„ฐ๋ฆฌ์˜ save() ๋ฉ”์„œ๋“œ์˜ ๋ธ”๋กœํ‚น๋˜๋Š” ํ˜ธ์ถœ์ด ๋๋‚˜๊ณ  ๋ณต๊ท€๋˜์–ด์•ผ postTaco()๊ฐ€ ๋๋‚˜๊ณ  ๋ณต๊ท€ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•œ๋‹ค. ์š”์ฒญ์€ ๋‘ ๋ฒˆ ๋ธ”๋กœํ‚น๋œ๋‹ค.

postTaco()๋กœ ์ง„์ž…ํ•  ๋•Œ์™€ postTaco()์˜ ๋‚ด๋ถ€์—์„œ๋‹ค.

@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
    return tacoRepo.save(taco);
}
  • ๊ทธ๋Ÿฌ๋‚˜ postTaco()์— ์กฐ๊ธˆ๋งŒ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ฝ”๋“œ๋ฅผ ์ ์šฉํ•˜๋ฉด ์™„์ „ํ•˜๊ฒŒ ๋ธ”๋กœํ‚น๋˜์ง€ ์•Š๋Š” ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.
  • saveAll() ๋ฉ”์„œ๋“œ๋Š” Mono๋‚˜ Flux๋ฅผ ํฌํ•จํ•ด์„œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ŠคํŠธ๋ฆผ์˜ publisher ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ์–ด๋–ค ํƒ€์ž…๋„ ์ธ์ž๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค.
  • saveAll() ๋ฉ”์„œ๋“œ๋Š” Flux๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.๊ทธ๋Ÿฌ๋‚˜ postTaco()์˜ ์ธ์ž๋กœ ์ „๋‹ฌ๋œ Mono๋ฅผ saveAll()์—์„œ ์ธ์ž๋กœ ๋ฐ›์•˜์œผ๋ฏ€๋กœ saveAll์ด ๋ฐ˜ํ™˜ํ•˜๋Š” Flux๊ฐ€ ํ•˜๋‚˜์˜ Taco๊ฐ์ฒด๋งŒ ํฌํ•จ๋˜์–ด์•ผ ํ•œ๋‹ค.
  • next()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Mono๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ด๊ฒƒ์„ postTaco๊ฐ€ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

(Flux๋Š” 0,1 ๋˜๋Š” ๋‹ค์ˆ˜์˜(๋ฌดํ•œ์ผ ์ˆ˜ ์žˆ๋Š”) ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ–๋Š” ํŒŒ์ดํ”„๋ผ์ธ์„ ๋‚˜ํƒ€๋‚ธ๋‹ค. ๋ฐ˜๋ฉด์— Mono๋Š” ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ ํ•ญ๋ชฉ๋งŒ ๊ฐ–๋Š” ๋ฐ์ดํ„ฐ์…‹์— ์ตœ์ ํ™”๋œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…)

@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
    return tacoRepo.saveAll(tacoMono).next();
}

11.2 ํ•จ์ˆ˜ํ˜• ์š”์ฒญ ํ•ธ๋“ค๋Ÿฌ ์ •์˜ํ•˜๊ธฐ

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

์Šคํ”„๋ง 5์—๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ API๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•œ ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ ์†Œ๊ฐœ

  • ์ƒˆ๋กœ์šด ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์€ ํ”„๋ ˆ์ž„์›Œํฌ๋ณด๋‹ค๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ๋˜๋ฏ€๋กœ ์• ๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์š”์ฒญ์„ ํ•ธ๋“ค๋Ÿฌ ์ฝ”๋“œ์— ์—ฐ๊ด€์‹œํ‚จ๋‹ค.
  • ์Šคํ”„๋ง์˜ ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•œ API์˜ ์ž‘์„ฑ์—๋Š” ๋‹ค์Œ ๋„ค๊ฐ€์ง€ ๊ธฐ๋ณธ ํƒ€์ž…์ด ์ˆ˜๋ฐ˜๋œ๋‹ค.
  • RequestPredicate: ์ฒ˜๋ฆฌ๋  ์š”์ฒญ์˜ ์ข…๋ฅ˜๋ฅผ ์„ ์–ธํ•œ๋‹ค.
  • RouterFunction: ์ผ์น˜ํ•˜๋Š” ์š”์ฒญ์ด ์–ด๋–ป๊ฒŒ ํ•ธ๋“ค๋Ÿฌ์—๊ฒŒ ์ „๋‹ฌ๋˜์–ด์•ผ ํ•˜๋Š”์ง€๋ฅผ ์„ ์–ธํ•œ๋‹ค.
  • ServerRequest: HTTP ์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ ํ—ค๋”์™€ ๋ชธ์ฒด ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ServerResponse: HTTP ์‘๋‹ต์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ ํ—ค๋”์™€ ๋ชธ์ฒด ์ •๋ณด๋ฅผ ํฌํ•จํ•œ๋‹ค.

helper class๋ฅผ static import: ํ•จ์ˆ˜ํ˜• ํƒ€์ž…์„ ์ƒ์„ฑํ•˜๋Š”๋ฐ ์‚ฌ์šฉ

package tacos.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import tacos.model.Taco;
import tacos.repository.TacoRepository;

import java.net.URI;

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class RouterFunctionConfig {

    @Autowired
    private TacoRepository  tacoRepo;

    @Bean
    public RouterFunction<?> routerFunction() {
        return route(GET("/design/taco"), this::recents)
                .andRoute(POST("/design"), this::postTaco);
    }

    public Mono<ServerResponse> recents(ServerRequest  request) {
        return ServerResponse.ok().body(tacoRepo.findAll().take(12), Taco.class);
    }

    public Mono<ServerResponse> postTaco(ServerRequest request) {
        Mono<Taco> taco  = request.bodyToMono(Taco.class);
        Mono<Taco> savedTaco =  taco.flatMap(tacoRepo::save);
        return ServerResponse.created(URI.create("http://localhost:8080/design/taco" + savedTaco.map(Taco::getId))).body(savedTaco, Taco.class);
    }
}

// https://stackoverflow.com/questions/47179937/how-to-get-string-from-monostring-in-reactive-java

Trouble shooting

  • Inferred type 'S' for type parameter 'S' is not within its bound; mono

[https://stackoverflow.com/questions/47918441/why-spring-reactivemongorepository-doest-have-save-method-for-mono] [https://jira.spring.io/browse/DATACMNS-1385]

11.3 ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํŠธ๋กค๋Ÿฌ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

  • ์Šคํ”„๋ง WebFlux๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ ์ปจํŠธ๋กค๋Ÿฌ์˜ ํ…Œ์ŠคํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ์ƒˆ๋กœ์šด ์œ ํ‹ธ๋ฆฌํ‹ฐ

11.3.1 GET ์š”์ฒญ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

  • recentTacos() ๋ฉ”์„œ๋“œ์— ๊ด€ํ•ด /design/recent ๊ฒฝ๋กœ์˜ HTTP GET ์š”์ฒญ์ด ์ƒ๊ธฐ๋ฉด 12๊ฐœ๊นŒ์ง€์˜ ํƒ€์ฝ”๋ฅผ ํฌํ•จํ•˜๋Š” JSON ํŽ˜์ด๋กœ๋“œ๊ฐ€ ์‘๋‹ต์— ํฌํ•จ๋˜์–ด์•ผ ํ•œ๋‹ค.

WebTestClient๋ฅผ ์‚ฌ์šฉํ•ด์„œ DesignTacoControllerTest ํ…Œ์ŠคํŠธ ํ•˜๊ธฐ

  • shouldReturnRecentTacos() ๋ฉ”์„œ๋“œ๋Š” Flux ํƒ€์ž…์˜ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  • ๋ชจ์˜(mock) TacoRepository์˜ findAll() ๋ฉ”์„œ๋“œ์˜ ๋ฐ˜ํ™˜ ๊ฐ’์œผ๋กœ ์ƒ์„ฑ๋œ Flux ์ œ๊ณต
  • Flux๊ฐ€ ๋ฐœํ–‰ํ•˜๋Š” Taco ๊ฐ์ฒด๋Š” testTaco()๋ผ๋Š” ์ด๋ฆ„์˜ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ์—์„œ ์ƒ์„ฑ๋˜๋ฉฐ ์ด ๋ฉ”์„œ๋“œ์—์„œ๋Š” ์ธ์ž๋กœ ๋ฐ›์€ ์ˆซ์ž๋กœ ID์™€ ์ด๋ฆ„์„ ๊ฐ–๋Š” Taco ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • shouldReturnRecentTacos() ๋ฉ”์„œ๋“œ์—์„œ๋Š” ๋ชจ์˜ TacoRepository๋ฅผ DesigTacoController์˜ ์ƒ์„ฑ์ž์— ์ฃผ์ž…ํ•˜์—ฌ ์ด ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  ์ด ์ธ์Šคํ„ด์Šค๋Š” WebTestClient.bindToController()์˜ ์ธ์ž๋กœ ์ „๋‹ฌ๋˜์–ด WebTestClient ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.
  • ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์ค€๋น„๊ฐ€ ์™„๋ฃŒ๋˜์–ด WebTestClient๋ฅผ ์‚ฌ์šฉํ•ด์„œ /design/recent ์˜ GET ์š”์ฒญ์„ ์ œ์ถœํ•˜๊ณ  ๊ธฐ๋Œ€ํ•˜๋Š” ์‘๋‹ต์ด ์˜ค๋Š”์ง€ ๊ฒ€์‚ฌํ•  ์ค€๋น„
  • get().uri("/design/recent")์˜ ํ˜ธ์ถœ์€ ์ œ์ถœ(submit)์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ ๊ทธ ๋‹ค์Œ์— exchange()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํ•ด๋‹น ์š”์ฒญ์„ ์ œ์ถœํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ด ์š”์ฒญ์€ WebTestClient์™€ ์—ฐ๊ฒฐ๋œ ์ปจํŠธ๋กค๋Ÿฌ์ธ DesignTacoController์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • ๋งˆ์ง€๋ง‰์œผ๋กœ ์š”์ฒญ ์‘๋‹ต์ด expectStatus()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์‘๋‹ต์ด HTTP 200(OK) ์ƒํƒœ ์ฝ”๋“œ๋ฅผ ๊ฐ–๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.
  • jsonPath()๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœํ•˜์—ฌ ์‘๋‹ต ๋ชธ์ฒด์˜ JSON์ด ๊ธฐ๋Œ€ํ•œ ๊ฐ’์„ ๊ฐ–๋Š”์ง€ ๊ฒ€์‚ฌํ•œ๋‹ค.

11.3.2 POST ์š”์ฒญ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

  • ๋ชจ๋“  ์ข…๋ฅ˜์˜ HTTP ๋ฉ”์„œ๋“œ๋ฅผ ํ…Œ์ŠคํŠธ ํ•˜๋Š” ๋ฐ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • /design์˜ POST ์š”์ฒญ์„ ์ œ์ถœํ•˜์—ฌ ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ API์˜ ํƒ€์ฝ” ์ƒ์„ฑ ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•œ๋‹ค.
package tacos.web;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.util.ArrayList;
import java.util.List;

import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import tacos.model.Ingredient;
import tacos.model.Ingredient.Type;
import tacos.model.Taco;
import tacos.repository.TacoRepository;

public class DesignTacoControllerTest {

    @Test
    public void shouldReturnRecentTacos() {
        Taco[] tacos = {
                testTaco(1L), testTaco(2L),
                testTaco(3L), testTaco(4L),
                testTaco(5L), testTaco(6L),
                testTaco(7L), testTaco(8L),
                testTaco(9L), testTaco(10L),
                testTaco(11L), testTaco(12L),
                testTaco(13L), testTaco(14L),
                testTaco(15L), testTaco(16L)};
        Flux<Taco> tacoFlux = Flux.just(tacos);

        TacoRepository tacoRepo = Mockito.mock(TacoRepository.class);
        when(tacoRepo.findAll()).thenReturn(tacoFlux);

        WebTestClient testClient = WebTestClient.bindToController(
                new DesignTacoController(tacoRepo))
                .build();

        testClient.get().uri("/design/recent")
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$").isArray()
                .jsonPath("$").isNotEmpty()
                .jsonPath("$[0].id").isEqualTo(tacos[0].getId().toString())
                .jsonPath("$[0].name").isEqualTo("Taco 1")
                .jsonPath("$[1].id").isEqualTo(tacos[1].getId().toString())
                .jsonPath("$[1].name").isEqualTo("Taco 2")
                .jsonPath("$[11].id").isEqualTo(tacos[11].getId().toString())
                .jsonPath("$[11].name").isEqualTo("Taco 12")
                .jsonPath("$[12]").doesNotExist();
    }

    @Test
    public void shouldSaveATaco() {
        TacoRepository tacoRepo = Mockito.mock(
                TacoRepository.class);
        Mono<Taco> unsavedTacoMono = Mono.just(testTaco(null));
        Taco savedTaco = testTaco(null);
        Mono<Taco> savedTacoMono = Mono.just(savedTaco);

        when(tacoRepo.save(any())).thenReturn(savedTacoMono);

        WebTestClient testClient = WebTestClient.bindToController(
                new DesignTacoController(tacoRepo)).build();

        testClient.post()
                .uri("/design")
                .contentType(MediaType.APPLICATION_JSON)
                .body(unsavedTacoMono, Taco.class)
                .exchange()
                .expectStatus().isCreated()
                .expectBody(Taco.class)
                .isEqualTo(savedTaco);
    }


    private Taco testTaco(Long number) {
        Taco taco = new Taco();
        taco.setId(number != null ? number.toString(): "TESTID");
        taco.setName("Taco " + number);
        List<Ingredient> ingredients = new ArrayList<>();
        ingredients.add(
                new Ingredient("INGA", "Ingredient A", Type.WRAP));
        ingredients.add(
                new Ingredient("INGB", "Ingredient B", Type.PROTEIN));
        taco.setIngredients(ingredients);
        return taco;
    }
}

11.3.3 ์‹คํ–‰ ์ค‘์ธ ์„œ๋ฒ„๋กœ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

  • ์ง€๊ธˆ๊นŒ์ง€ ์‚ฌ์šฉํ–ˆ๋˜ ํ…Œ์ŠคํŠธ๋Š” ๋ชจ์˜ ์Šคํ”„๋ง WebFlux ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ–ˆ์œผ๋ฏ€๋กœ ์‹ค์ œ ์„œ๋ฒ„๊ฐ€ ํ•„์š” ์—†์—ˆ๋‹ค.
  • Netty๋‚˜ ํ†ฐ์บฃ๊ณผ ๊ฐ™์€ ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ๋ฆฌํผ์ง€ํ† ๋ฆฌ๋‚˜ ๋‹ค๋ฅธ ์˜์กด์„ฑ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•ด์„œ WebFlux ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ํ…Œ์ŠคํŠธํ•  ํ•„์š”๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค.
  • ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.
  • WebTestClient์˜ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด @RunWith(SpringRunner.class), @SpringBootTest ์• ๋…ธํ…Œ์ด์…˜ ์ž‘์„ฑ
  • ๋ฌด์ž‘์œ„๋กœ ์„ ํƒ๋œ ํฌํŠธ๋กœ ์‹คํ–‰ ์„œ๋ฒ„๊ฐ€ ๋ฆฌ์Šค๋‹ํ•˜๋„๋ก ์Šคํ”„๋ง์— ์š”์ฒญ
  • @Autowired๋ฅผ ์ง€์ •ํ•˜์—ฌ WebTestClient๋ฅผ ํ…Œ์ŠคํŠธ ํด๋ž˜์Šค๋กœ ์ž๋™ ์—ฐ๊ฒฐ
  • ํ…Œ์ŠคํŠธ ๋ฉ”์†Œ๋“œ์—์„œ WebTestClient ์ธ์Šคํ„ด์Šค๋ฅผ ๋” ์ด์ƒ ์ƒ์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์Œ. ์š”์ฒญํ•  ๋•Œ ์™„์ „ํ•œ URL์„ ์ง€์ •ํ•  ํ•„์š”๋„ ์—†๋‹ค.
  • ํƒœ์ŠคํŠธ ์„œ๋ฒ„๊ฐ€ ์–ด๋–ค ํฌํŠธ์—์„œ ์‹คํ–‰ ์ค‘์ธ์ง€ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ WebTestClient๊ฐ€ ์„ค์ •๋˜๊ธฐ ๋•Œ๋ฌธ.
  • ์ž๋™ ์—ฐ๊ฒฐ๋˜๋Š” ์ธ์Šคํ„ด์Šค๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— WebTestClient ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.
  • ์Šคํ”„๋ง์ด DesignTacoController์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์‹ค์ œ TacoRespository๋ฅผ ์ฃผ์ž…ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋” ์ด์ƒ ๋ชจ์˜ TacoRespository๋„ ํ•„์š” ์—†๋‹ค.
  • JSONPath ํ‘œํ˜„์‹์„ ํ†ตํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ๋ถ€ํ„ฐ ๊ฐ€์ ธ์˜จ ๊ฐ’์„ ๊ฒ€์‚ฌ.
package tacos.web;


import java.io.IOException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient  // Could not autowire. No beans of 'WebTestClient' type found
public class DesignTacoControllerWebTest {

    @Autowired
    private WebTestClient testClient;

    @Test
    public void shouldReturnRecentTacos() throws IOException {
        testClient.get().uri("/design/recent")
                .accept(MediaType.APPLICATION_JSON).exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$[?(@.id == 'TACO1')].name")
                .isEqualTo("Carnivore")
                .jsonPath("$[?(@.id == 'TACO2')].name")
                .isEqualTo("Bovine Bounty")
                .jsonPath("$[?(@.id == 'TACO3')].name")
                .isEqualTo("Veg-Out");
    }

}

11.4 REST API๋ฅผ ๋ฆฌ์•กํ‹ฐ๋ธŒํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธฐ

  • ์Šคํ”„๋ง ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น์˜ ํด๋ผ์ด์–ธํŠธ ์ธก๋ฉด์— ๊ด€์‹ฌ์„ ๋Œ๋ ค์„œ Mono๋‚˜ Flux๊ฐ™์€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜๋Š” REST ํด๋ผ์ด์–ธํŠธ๋ฅผ WebClient๊ฐ€ ์–ด๋–ป๊ฒŒ ์ œ๊ณตํ•˜๋Š”์ง€ ์•Œ์•„๋ณธ๋‹ค.
  • 7์žฅ์—์„œ๋Š” RestTemplate๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ API์˜ ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์„ ํ•˜์˜€๋‹ค.
  • ์Šคํ”„๋ง 3.0์—์„œ ์†Œ๊ฐœ๋˜์—ˆ๋˜ RestTemplate์€ ์ด์ œ ๊ตฌ์„ธ๋Œ€๊ฐ€ ๋˜์—ˆ๋‹ค. ๊ทธ ๋‹น์‹œ์—๋Š” ๋งŽ์€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ฌด์ˆ˜ํ•œ ์š”์ฒญ์— RestTemplate์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ RestTemplate์ด ์ œ๊ณตํ•˜๋Š” ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋Š” ๋ฆฌ์•กํ‹ฐ๋ธŒ๊ฐ€ ์•„๋‹Œ ๋„๋ฉ”์ธ ํƒ€์ž…์ด๋‚˜ ์ปฌ๋ ‰์…˜์„ ์ฒ˜๋ฆฌํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋ฐฉ์‹์œผ๋กœ ์‘๋‹ต ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด ์ด๊ฒƒ์„ Flux๋‚˜ Mono ํƒ€์ž…์œผ๋กœ ๋ž˜ํ•‘ํ•ด์•ผ ํ•œ๋‹ค.

๋”ฐ๋ผ์„œ RestTemplate์„ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ์Šคํ”„๋ง 5๊ฐ€ RestTemplate์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋Œ€์•ˆ์œผ๋กœ WebClient๋ฅผ ์ œ๊ณตํ•จ.

  • WebClient๋Š” ์™ธ๋ถ€ API๋กœ ์š”์ฒญ์„ ํ•  ๋•Œ ๋ฆฌ์•กํ‹ฐ๋ธŒ ํƒ€์ž…์˜ ์ „์†ก๊ณผ ์ˆ˜์‹  ๋ชจ๋‘๋ฅผ ํ•œ๋‹ค.
  • RestTemplate๋Š” ๋‹ค์ˆ˜์˜ ๋ฉ”์„œ๋“œ๋กœ ์„œ๋กœ ๋‹ค๋ฅธ ์ข…๋ฅ˜์˜ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋Œ€์‹  WebClient๋Š” ์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๊ณ  ์ „์†กํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๋นŒ๋” ๋ฐฉ์‹์˜ ์ธํ„ฐํŽ˜์ด์Šค ์‚ฌ์šฉ

WebClient๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ํŒจํ„ด

  • WebClient์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค (๋˜๋Š” WebClient ๋นˆ์„ ์ฃผ์ž…ํ•œ๋‹ค.)
@Bean
public WebClient webClient() {
   return WebClient.create("http://localhost:8080");
}
  • ์š”์ฒญ์„ ์ „์†กํ•  HTTP ๋ฉ”์†Œ๋“œ๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  • ์š”์ฒญ์— ํ•„์š”ํ•œ URI์™€ ํ—ค๋”๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  • ์š”์ฒญ์„ ์ œ์ถœํ•œ๋‹ค.
  • ์‘๋‹ต์„ ์†Œ๋น„(์‚ฌ์šฉ)ํ•œ๋‹ค.

11.4.1 ๋ฆฌ์†Œ์Šค ์–ป๊ธฐ(GET)

  • ํƒ€์ฝ” ํด๋ผ์šฐ๋“œ API๋กœ๋ถ€ํ„ฐ ์‹์ž์žฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํŠน์ • Ingredient ๊ฐ์ฒด๋ฅผ ์ด๊ฒƒ์˜ ID๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค๊ณ  ํ•ด๋ณด์ž.
  • RestTemplate์˜ ๊ฒฝ์šฐ๋Š” getForObject() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • ๊ทธ๋Ÿฌ๋‚˜ WebClient๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์š”์ฒญ์„ ์ƒ์„ฑํ•˜๊ณ  ์‘๋‹ต์„ ๋ฐ›์€ ๋‹ค์Œ์— Ingredient ๊ฐ์ฒด๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” Mono๋ฅผ ์ถ”์ถœํ•œ๋‹ค.
  • create() ๋ฉ”์„œ๋“œ๋กœ ์ƒˆ๋กœ์šด WebClient ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑ
  • get()๊ณผ url๋ฅผ ์‚ฌ์šฉํ•ด์„œ GET ์š”์ฒญ์„ ์ •์˜
  • retrieve() ๋ฉ”์„œ๋“œ๋Š” ํ•ด๋‹น ์š”์ฒญ์„ ์‹คํ–‰
  • ๋งˆ์ง€๋ง‰์œผ๋กœ bodyToMono() ํ˜ธ์ถœ์—์„œ๋Š” ์‘๋‹ต ๋ชธ์ฒด์˜ ํŽ˜์ด๋กœ๋“œ๋ฅผ Mono๋กœ ์ถ”์ถœํ•œ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์ด ์ฝ”๋“œ๋Š” ๋‹ค์Œ์—๋Š” ๊ณ„์†ํ•ด์„œ Mono์˜ ๋‹ค๋ฅธ ์˜คํผ๋ ˆ์ด์…˜๋“ค์„ ์—ฐ์‡„ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.
  • bodyToMono()๋กœ๋ถ€ํ„ฐ ๋ฐ˜ํ™˜๋˜๋Š” Mono์— ์ถ”๊ฐ€๋กœ ์˜คํผ๋ ˆ์ด์…˜์„ ์ ์šฉํ•˜๋ ค๋ฉด ํ•ด๋‹น ์š”์ฒญ์ด ์ „์†ก๋˜๊ธฐ ์ „์— ๊ตฌ๋…์„ ํ•ด์•ผ ํ•œ๋‹ค.
  • ๋‹ค์ˆ˜์˜ ํ•ญ๋ชฉ์„ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์€ ๋‹จ์ผ ํ•ญ๋ชฉ์„ ์š”์ฒญํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•˜๋‹ค. bodyToFlux()๋ฅผ ์‚ฌ์šฉํ•ด์„œ Flux๋กœ ์ถ”์ถœํ•  ๊ฒƒ.
Mono<Ingredient> ingredient = WebClient.create()
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);

ingredient.subscribe(i -> {...})

11.4.2 ๋ฆฌ์†Œ์Šค ์ „์†กํ•˜๊ธฐ

  • WebClient๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ ์ˆ˜์‹ ๊ณผ ํฌ๊ฒŒ ๋‹ค๋ฅด์ง€ ์•Š๋‹ค.
  • get() ๋Œ€์‹  post() ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  body()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ Mono๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ•ด๋‹น ์š”์ฒญ ๋ชธ์ฒด์— ๋„ฃ๋Š” ๋‹ค๋Š” ๊ฒƒ๋งŒ ์ง€์ •ํ•˜๋ฉด ๋œ๋‹ค.
Mono<Ingredient> ingredientMono = ...;
body(ingredientMono, Ingredient.class)

11.5 ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น API ๋ณด์•ˆ

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ์›น ๋ณด์•ˆ ๋ชจ๋ธ์€ ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ๋ฅผ ์ค‘์‹ฌ์œผ๋กœ ๋งŒ๋“ค์–ด์กŒ๋‹ค.
  • ๋งŒ์ผ ์š”์ฒญ์ž๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ถŒํ•œ์„ ๊ฐ–๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์„œ๋ธ”๋ฆฟ ๊ธฐ๋ฐ˜ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์š”์ฒญ ๋ฐ”์šด๋“œ๋ฅผ(ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์„ ์„œ๋ธ”๋ฆฟ์ด ๋ฐ›๊ธฐ ์ „์—) ๊ฐ€๋กœ์ฑ„์•ผ ํ•œ๋‹ค๋ฉด ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ๊ฐ€ ํ™•์‹คํ•œ ์„ ํƒ์ด๋‚˜ ์Šคํ”„๋ง WebFlux์—์„œ๋Š” ๋ถˆ๊ฐ€๋Šฅ
  • ์Šคํ”„๋ง WebFlux๋กœ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ž‘์„ฑํ•  ๋•Œ๋Š”์„œ๋ธ”๋ฆฟ์ด ๊ฐœ์ž…๋œ๋‹ค๋Š” ๋ณด์žฅ์ด ์—†๋‹ค.
  • ์‹ค์ œ๋กœ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ Netty๋‚˜ ์ผ๋ถ€ ๋‹ค๋ฅธ non-์„œ๋ธ”๋ฆฟ ์„œ๋ฒ„์— ๊ตฌ์ถ•๋  ๊ฐ€๋Šฅ์„ฑ์ด ๋งŽ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ ๊ธฐ๋ฐ˜์˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์Šคํ”„๋ง WebFlux ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์—†๋Š” ๊ฒƒ์ผ๊นŒ?

  • ์Šคํ”„๋ง 5.0.0 ๋ฒ„์ „๋ถ€ํ„ฐ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์„œ๋ธ”๋ฆฟ ๊ธฐ๋ฐ˜์˜ ์Šคํ”„๋ง MVC์™€ ๋ฆฌ์•กํ‹ฐ๋ธŒ ์Šคํ”„๋ง WebFlux ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ชจ๋‘์˜ ๋ณด์•ˆ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค.
  • WebFilter๊ฐ€ ์ด ์ผ์„ ํ•ด์ค€๋‹ค.
  • WebFilter๋Š” ์„œ๋ธ”๋ฆฟ API์— ์˜์กดํ•˜์ง€ ์•Š๋Š” ์Šคํ”„๋ง ํŠน์œ ์˜ ์„œ๋ธ”๋ฆฟ ํ•„ํ„ฐ ๊ฐ™์€ ๊ฒƒ.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .antMatchers("/design", "/orders")
                .access("hasRole('ROLE_USER')")
                .antMatchers("/", "/**").access("permitAll")
                .and()
                .formLogin()
                .loginPage("/login")
                .and()
                .logout()
                .logoutSuccessUrl("/")
                .and()
                .csrf();
    }

์Šคํ”„๋ง WebFlux ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๊ตฌ์„ฑํ•˜๊ธฐ

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain  securityWebFilterChain(ServerHttpSecurity http) {
        return http
                .authorizeExchange()
                .pathMatchers("/design", "orders").hasAuthority("USER")
                .anyExchange().permitAll()
                .and()
                .build();
    }

11.5.2 ๋ฆฌ์•กํ‹ฐ๋ธŒ ์‚ฌ์šฉ์ž ๋ช…์„ธ ์„œ๋น„์Šค ๊ตฌ์„ฑํ•˜๊ธฐ

  • ReactiveUserDetailsService ๋นˆ์„ ์„ ์–ธํ•˜์˜€๋‹ค. ์ด๊ฒƒ์€ UserDetailsService์˜ ๋ฆฌ์•กํ‹ฐ๋ธŒ ๋ฒ„์ „์ด๋ฉฐ UserDetailsService์ฒ˜๋Ÿผ ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๋งŒ ๊ตฌํ˜„ํ•˜๋ฉด ๋œ๋‹ค.
  • findByUsername() ๋ฉ”์„œ๋“œ๋Š” UserDetails๊ฐ์ฒด ๋Œ€์‹  Mono๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ์—ฌ๊ธฐ์„œ UserRepository(์ฝ”๋“œ์—์„œ๋Š” userRepo ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ฐธ์กฐ๋จ)์˜ findByUsername() ๋ฉ”์„œ๋“œ๋Š” Mono๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ๋”ฐ๋ผ์„œ Mono ํƒ€์ž…์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์˜คํผ๋ ˆ์ด์…˜๋“ค(์˜ˆ๋ฅผ ๋“ค์–ด map())์„ ์—ฐ์‡„์ ์œผ๋กœ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.
  • map() ์˜คํผ๋ ˆ์ด์…˜์˜ ์ธ์ž๋กœ ๋žŒ๋‹ค๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํ˜ธ์ถœํ•˜๋ฉฐ ์ด ๋žŒ๋‹ค์—์„œ๋Š” UserRepository.findByUsername()์—์„œ ๋ฐ˜ํ™˜๋œ Mono๊ฐ€ ๋ฐœํ–‰ํ•˜๋Š” User ๊ฐ์ฒด์˜ toUserDetails() ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  • ์ด ๋ฉ”์„œ๋“œ๋Š” User๊ฐ์ฒด๋ฅผ UserDetails ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค

๋”ฐ๋ผ์„œ map() ์˜คํผ๋ ˆ์ด์…˜์—์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ํƒ€์ž…์€ Mono ๊ฐ€ ๋œ๋‹ค. ์ด๊ฒƒ์ด ReactiveUserDetailsService.findByUsername()์—์„œ ์š”๊ตฌํ•˜๋Š” ๋ฐ˜ํ™˜ ํƒ€์ž…์ด๋‹ค.

@Service
public ReactiveUserDetailsService UserDetailsService(UserRepository userRepo) {
   return new ReactiveUserDetailsService() {
       @Override
       public Mono<UserDetails> findByUsername(String username)  {
            return userRepo.findByUsername(username)
                   .map(user -> {
                       return user.toUserDetails();
                   });
       }
   };
}

[์ฐธ๊ณ ]

โš ๏ธ **GitHub.com Fallback** โš ๏ธ