chap 11 - JAVA-JIKIMI/SPRING-IN-ACTION-5 GitHub Wiki
์คํ๋ง WebFlux๋ ์คํ๋ง MVC์ ๋์์ด๋ฉฐ ์คํ๋ง MVC์ ๋์ผํ ๊ฐ๋ฐ ๋ชจ๋ธ์ ์ฌ์ฉํด์ด ๋ฆฌ์กํฐ๋ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ์์ฑํ ์ ์๋ ์ ํ์ ๊ธฐํ๋ฅผ ์ ๊ณต
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 ์์ฑ์ ๊ดํด ์ฐ๋ฆฌ๊ฐ ์ด๋ฏธ ์๊ณ ์๋ ๊ฒ์ ๋ง์ ๋ถ๋ถ์ ํ์ฉํ ์ ์๋ค.
- ๋งค ์ฐ๊ฒฐ๋ง๋ค ํ๋์ ์ค๋ ๋๋ฅผ ์ฌ์ฉํ๋ ์คํ๋ง MVC ๊ฐ์์ ํ์ ์ธ ์๋ธ๋ฆฟ ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ๋ ์ค๋ ๋ ๋ธ๋กํน(์ฐจ๋จ)๊ณผ ๋ค์ค ์ค๋ ๋๋ก ์ํ๋๋ค.
- ๋ฐ๋ผ์ ๋ธ๋กํน ์น ํ๋ ์์ํฌ๋ ์์ฒญ๋์ ์ฆ๊ฐ์ ๋ฐ๋ฅธ ํ์ฅ์ด ์ฌ์ค์ ์ด๋ ต๋ค. ๊ฒ๋ค๊ฐ ์ฒ๋ฆฌ๊ฐ ๋๋ฆฐ ์์ ์ค๋ ๋๋ก ์ธํด ํจ์ฌ ๋ ์ฌ๊ฐํ ์ํฉ์ด ๋ฐ์ํ๋ค.
- ํด๋น ์์ ์ค๋ ๋๊ฐ ํ๋ก ๋ฐํ๋์ด ๋ค๋ฅธ ์์ฒญ ์ฒ๋ฆฌ๋ฅผ ์ค๋นํ๋ ๋ฐ ๋ ๋ง์ ์๊ฐ์ด ๊ฑธ๋ฆฌ๊ธฐ ๋๋ฌธ์ด๋ค. ์ํฉ์ ๋ฐ๋ผ์๋ ์ด ๋ฐฉ์์ด ์๋ฒฝํ ๋ฐ์๋ค์ผ ๋งํ๋ค.
- ์ค์ ๋ก ์ด๊ฒ์ ๋๋ถ๋ถ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ๊ฐ๋ฐ์ 10๋ ๋๊ฒ ์ฌ์ฉ๋ ๋ฐฉ๋ฒ์ด๋ค. ๊ทธ๋ฌ๋ ์๋๊ฐ ๋ฐ๋๊ณ ์๋ค.
- ๋ ์ ์ ์์ ์ค๋ ๋(์ผ๋ฐ์ ์ผ๋ก CPU ์ฝ์ด๋น ํ๋)๋ก ๋ ๋์ ํ์ฅ์ฑ์ ์ฑ์ทจํ๋ค.
- ์ด๋ฒคํธ ๋ฃจํ(event loop) ์ด๋ผ๋ ๊ธฐ๋ฒ์ ์ ์ฉํ ์ด๋ฐ ํ๋ ์์ํฌ๋ ํ ์ค๋ ๋๋น ๋ง์ ์์ฒญ์ ์ฒ๋ฆฌํ ์ ์์ด์ ํ ์ฐ๊ฒฐ๋น ์์ ๋น์ฉ์ด ๋ ๊ฒฝ์ ์ ์ด๋ค.
- ๋น์ฉ์ด ๋๋ ์์ ์ด ํ์ํ ๋ ์ด๋ฒคํธ ๋ฃจํ๋ ํด๋น ์์ ์ ์ฝ๋ฐฑ(call back)์ ๋ฑ๋กํ์ฌ ๋ณํ์ผ๋ก ์ํ๋๊ฒ ํ๊ณ ๋ค๋ฅธ ์ด๋ฒคํธ ์ฒ๋ฆฌ๋ก ๋์ด๊ฐ๋ค.
- ์คํ๋ง 5๋ WebFlux๋ผ๋ ์๋ก์ด ์น ํ๋ ์์ํฌ๋ก ๋ฆฌ์กํฐ๋ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ Flux๋ ์คํ๋ง MVC์ ํต์ฌ ์ปดํฌ๋ํธ๋ฅผ ๊ณต์ ํ๋ค.
- Spring MVC๋ ์๋ฐ ์๋ธ๋ฆฟ API์ ์์ ๊ณ์ธต์ ์์น
- Spring WebFlux ๋ ์๋ธ๋ฆฟ API์ ์ฐ๊ณ๋์ง ์๋๋ค. ์๋ธ๋ฆฟ API๊ฐ ์ ๊ณตํ๋ ๊ฒ๊ณผ ๋์ผํ ๊ธฐ๋ฅ์ ๋ฆฌ์กํฐ๋ธ ๋ฒ์ ์ธ ๋ฆฌ์กํฐ๋ธ HTTP API์ ์์ ๊ณ์ธต์ ์์นํ๋ค.
- Spring WebFlux๋ ์๋ธ๋ฆฟ API์ ์ฐ๊ฒฐ๋์ง ์์ผ๋ฏ๋ก ์คํํ๊ธฐ ์ํด ์๋ธ๋ฆฟ ์ปจํ ์ด๋๋ฅผ ํ์๋ก ํ์ง ์๋๋ค.
- ๋์ ์ ๋ธ๋กํน์ด ์๋ ์ด๋ค ์น ์ปจํ ์ด๋์์๋ ์คํ๋ ์ ์์ผ๋ฉฐ ์ด์๋ Netty, Undertow, ํฐ์บฃ, Jetty ๋๋ ๋ค๋ฅธ ์๋ธ๋ฆฟ 3.1 ์ด์์ ์ปจํ ์ด๋๊ฐ ํฌํจ๋๋ค.
- ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ํจ๋ฌ๋ค์์ผ๋ก ์ปจํธ๋กค๋ฌ๋ฅผ ์ ์ํ๋ ๋์ ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ์ ๋ํ๋ธ๋ค.
- 6์ฅ์์ ํ์ฝ ํด๋ผ์ฐ๋์ REST API ์ปจํธ๋กค๋ฌ ์์ฑ์ ๋ฆฌ์กํฐ๋ธ๋ฅผ ๋ณ๊ฒฝ
- ์์ฒญ ์ฒ๋ฆฌ ๋ฉ์๋๋ฅผ ๊ฐ์ง๊ณ ์๋ ์ปจํธ๋กค๋ฌ - ๋๋ฉ์ธ ํ์ (Taco, Order) ๋๋ ๋๋ฉ์ธ ํ์ ์ ์ปฌ๋ ์ ์ผ๋ก ์ ๋ ฅ๊ณผ ์ถ๋ ฅ์ ์ฒ๋ฆฌ
- Iterable์ ๋ฆฌ์กํฐ๋ธ ํ์ ์ด ์๋๋ฉฐ Iterable์๋ ์ด๋ค ๋ฆฌ์กํฐ๋ธ ์คํผ๋ ์ด์ ๋ ์ ์ฉํ ์ ์์
- ํ๋ ์์ํฌ๊ฐ Iterable ํ์ ์ ๋ฆฌ์กํฐ๋ธ ํ์ ์ผ๋ก ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ์ค๋ ๋์ ๊ฑธ์ณ ์์ ์ ๋ถํ ํ๊ฒ ํ ์๋ ์๋ค.
- recentTacos()๊ฐ Flux ํ์ ์ ๋ฐํํ๊ฒ ํ๋ ๊ฒ์ด๋ค.
- ์คํ๋ง 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();
}
- ์คํ๋ง 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
- 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]
- ์คํ๋ง WebFlux๋ฅผ ์ฌ์ฉํ๋ ๋ฆฌ์กํฐ๋ธ ์ปจํธ๋กค๋ฌ์ ํ ์คํธ๋ฅผ ์ฝ๊ฒ ์์ฑํ๊ฒ ํด์ฃผ๋ ์๋ก์ด ์ ํธ๋ฆฌํฐ
- recentTacos() ๋ฉ์๋์ ๊ดํด /design/recent ๊ฒฝ๋ก์ HTTP GET ์์ฒญ์ด ์๊ธฐ๋ฉด 12๊ฐ๊น์ง์ ํ์ฝ๋ฅผ ํฌํจํ๋ JSON ํ์ด๋ก๋๊ฐ ์๋ต์ ํฌํจ๋์ด์ผ ํ๋ค.
- 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์ด ๊ธฐ๋ํ ๊ฐ์ ๊ฐ๋์ง ๊ฒ์ฌํ๋ค.
- ๋ชจ๋ ์ข ๋ฅ์ 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;
}
}
- ์ง๊ธ๊น์ง ์ฌ์ฉํ๋ ํ ์คํธ๋ ๋ชจ์ ์คํ๋ง 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");
}
}
- ์คํ๋ง ๋ฆฌ์กํฐ๋ธ ์น์ ํด๋ผ์ด์ธํธ ์ธก๋ฉด์ ๊ด์ฌ์ ๋๋ ค์ Mono๋ Flux๊ฐ์ ๋ฆฌ์กํฐ๋ธ ํ์ ์ ์ฌ์ฉํ๋ REST ํด๋ผ์ด์ธํธ๋ฅผ WebClient๊ฐ ์ด๋ป๊ฒ ์ ๊ณตํ๋์ง ์์๋ณธ๋ค.
- 7์ฅ์์๋ RestTemplate๋ฅผ ์ฌ์ฉํด์ ํ์ฝ ํด๋ผ์ฐ๋ API์ ํด๋ผ์ด์ธํธ ์์ฒญ์ ํ์๋ค.
- ์คํ๋ง 3.0์์ ์๊ฐ๋์๋ RestTemplate์ ์ด์ ๊ตฌ์ธ๋๊ฐ ๋์๋ค. ๊ทธ ๋น์์๋ ๋ง์ ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ฌด์ํ ์์ฒญ์ RestTemplate์ ์ฌ์ฉํ๋ค.
๊ทธ๋ฌ๋ RestTemplate์ด ์ ๊ณตํ๋ ๋ชจ๋ ๋ฉ์๋๋ ๋ฆฌ์กํฐ๋ธ๊ฐ ์๋ ๋๋ฉ์ธ ํ์ ์ด๋ ์ปฌ๋ ์ ์ ์ฒ๋ฆฌํ๋ค. ๋ฐ๋ผ์ ๋ฆฌ์กํฐ๋ธ ๋ฐฉ์์ผ๋ก ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ๊ณ ์ ํ๋ค๋ฉด ์ด๊ฒ์ Flux๋ Mono ํ์ ์ผ๋ก ๋ํํด์ผ ํ๋ค.
๋ฐ๋ผ์ RestTemplate์ ๋ฆฌ์กํฐ๋ธ ํ์ ์ผ๋ก ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ผ๋ก ์คํ๋ง 5๊ฐ RestTemplate์ ๋ฆฌ์กํฐ๋ธ ๋์์ผ๋ก WebClient๋ฅผ ์ ๊ณตํจ.
- WebClient๋ ์ธ๋ถ API๋ก ์์ฒญ์ ํ ๋ ๋ฆฌ์กํฐ๋ธ ํ์ ์ ์ ์ก๊ณผ ์์ ๋ชจ๋๋ฅผ ํ๋ค.
- RestTemplate๋ ๋ค์์ ๋ฉ์๋๋ก ์๋ก ๋ค๋ฅธ ์ข ๋ฅ์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋์ WebClient๋ ์์ฒญ์ ๋ํ๋ด๊ณ ์ ์กํ๊ฒ ํด์ฃผ๋ ๋น๋ ๋ฐฉ์์ ์ธํฐํ์ด์ค ์ฌ์ฉ
- WebClient์ ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ค (๋๋ WebClient ๋น์ ์ฃผ์ ํ๋ค.)
@Bean
public WebClient webClient() {
return WebClient.create("http://localhost:8080");
}
- ์์ฒญ์ ์ ์กํ HTTP ๋ฉ์๋๋ฅผ ์ง์ ํ๋ค.
- ์์ฒญ์ ํ์ํ URI์ ํค๋๋ฅผ ์ง์ ํ๋ค.
- ์์ฒญ์ ์ ์ถํ๋ค.
- ์๋ต์ ์๋น(์ฌ์ฉ)ํ๋ค.
- ํ์ฝ ํด๋ผ์ฐ๋ 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 -> {...})
- WebClient๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๊ฒ์ ๋ฐ์ดํฐ ์์ ๊ณผ ํฌ๊ฒ ๋ค๋ฅด์ง ์๋ค.
- get() ๋์ post() ๋ฉ์๋๋ฅผ ์ฌ์ฉํ๊ณ body()๋ฅผ ํธ์ถํ์ฌ Mono๋ฅผ ์ฌ์ฉํด์ ํด๋น ์์ฒญ ๋ชธ์ฒด์ ๋ฃ๋ ๋ค๋ ๊ฒ๋ง ์ง์ ํ๋ฉด ๋๋ค.
Mono<Ingredient> ingredientMono = ...;
body(ingredientMono, Ingredient.class)
- ์คํ๋ง ์ํ๋ฆฌํฐ์ ์น ๋ณด์ ๋ชจ๋ธ์ ์๋ธ๋ฆฟ ํํฐ๋ฅผ ์ค์ฌ์ผ๋ก ๋ง๋ค์ด์ก๋ค.
- ๋ง์ผ ์์ฒญ์๊ฐ ์ฌ๋ฐ๋ฅธ ๊ถํ์ ๊ฐ๊ณ ์๋์ง ํ์ธํ๊ธฐ ์ํด ์๋ธ๋ฆฟ ๊ธฐ๋ฐ ์น ํ๋ ์์ํฌ์ ์์ฒญ ๋ฐ์ด๋๋ฅผ(ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ์๋ธ๋ฆฟ์ด ๋ฐ๊ธฐ ์ ์) ๊ฐ๋ก์ฑ์ผ ํ๋ค๋ฉด ์๋ธ๋ฆฟ ํํฐ๊ฐ ํ์คํ ์ ํ์ด๋ ์คํ๋ง 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();
}
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/design", "orders").hasAuthority("USER")
.anyExchange().permitAll()
.and()
.build();
}
- 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();
});
}
};
}
[์ฐธ๊ณ ]