- ์คํ๋ง WebFlux ์ฌ์ฉํ๊ธฐ
- ๋ฆฌ์กํฐ๋ธ ์ปจํธ๋กค๋ฌ์ ํด๋ผ์ด์ธํธ ์์ฑํ๊ณ ํ ์คํธํ๊ธฐ
- REST API ์๋นํ๊ธฐ
- ๋ฆฌ์กํฐ๋ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ณด์
-
์ ํ์ ์ธ ์๋ธ๋ฆฟ ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ(Spring MVC..)๋ ์ค๋ ๋ ๋ธ๋กํน์ด ๋ฐ์ ๊ฐ๋ฅ์ฑ์ด ์กด์ฌ, ๋ค์ค ์ค๋ ๋๋ก ์ํ
-
์ค๋ ๋ ํ ์ด๋ผ๋ ์์ ์ค๋ ๋์ ๋ชจ์์์ ์์ฒญ์ ๋ฐ์ ๋๋ง๋ค ํ๋์ฉ ๊ฐ์ ธ์ ์ฌ์ฉํ๋ ๊ฒ, ์ด ๋ ์์ฒญ ์ค๋ ๋์ ๊ฒฝ์ฐ ์์ ์ ๋ง์น๊ธฐ ์ ๊น์ง๋ ๋ธ๋กํน
-
๋ฐ๋ผ์ ์๋ธ๋ฆฟ ๊ธฐ๋ฐ์ ์น ํ๋ ์์ํฌ๋ ํ์ฅ์ฑ์ด ํฌ๊ฒ ๋จ์ด์ง
-
์ดํด ๋ฐํด์ ๋น๋๊ธฐ ์น ํ๋ ์์ํฌ์ ๊ฒฝ์ฐ ๋ ์ ์ ์์ ์ค๋ ๋๋ก ๋ ๋์ ํ์ฅ์ฑ์ ์ฑ์ทจ
-
์ฌ๊ธฐ์ ์ด๋ฒคํธ ๋ฃจํ ์ด๋ผ๋ ๊ธฐ๋ฒ์ ์ฌ์ฉ(Node.JS, NginX๊ฐ ๋ํ์ ์ธ ์ฌ๋ก)
- ์ด๋ฒคํธ ๋ฃจํ๋ ๋น์ฉ์ด ๋ค๋งํ ์์ ๋ค์ ์ฝ๋ฐฑ์ผ๋ก ๋ฑ๋กํ์ฌ ๋ณํ์ผ๋ก ์ํ, ๋ค์ ์ด๋ฒคํธ๋ก ๋์ด๊ฐ๊ณ ์ด๋ฅผ ๋ฐ๋ณต
- ๋ฐ๋ผ์ ์์์ ์ค๋ ๋๋ก ๋ ๋ง์ ์์ ๋ค์ ํ ์ ์๊ณ ์ค๋ ๋์ ๊ด๋ฆฌ ๋ถ๋ด์ด ์ค์ด๋๋ฉฐ ํ์ฅ์ด ์ฉ์ด
-
์คํ๋ง 5์์ ๋น๋๊ธฐ(ํ๋ก์ ํธ ๋ฆฌ์กํฐ ๊ธฐ๋ฐ) ์น ํ๋ ์์ํฌ(Spring WebFlux)๊ฐ ์๊ฐ
-
๋ง์ ๊ฒ์ Spring MVC์์ ๊ฐ์ ธ์ ๋ณ๋์ ๋ฆฌ์กํฐ๋ธ ์น ํ๋ ์์ํฌ๋ก ๋ง๋ฆ, Spring WebFlux๊ฐ ๊ทธ ์ฐ๋ฌผ
-
Spring MVC vs Spring WebFlux
- Spring WebFlux์์๋ Netty, ํฐ์บฃ, Jetty ๋ฑ ๋ค์ํ ๋ ผ๋ธ๋ฌํน ์น ์ปจํ ์ด๋์์ ์คํ ๊ฐ๋ฅ
- ์ผ์ชฝ ๋งจ ์์ Spring MVC์์ ์ฌ์ฉ๋๋ ์ปดํฌ๋ํธ๋ค์ Spring WebFlux์์๋ ์ฌ์ฉ์ด ๊ฐ๋ฅ(์ฃผ๋ก ์ปจํธ๋กค๋ฌ๋ฅผ ์ ์ํ๋ ๋ฐ ์ฌ์ฉ๋๋ ์ ๋ ธํ ์ด์ ๋ค)
- RouterFunction์ ๊ฒฝ์ฐ, ์ ๋ ธํ ์ด์ ์ ๋์ ํ์ฌ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ํจ๋ฌ๋ค์์ผ๋ก ์ปจํธ๋กค๋ฌ๋ฅผ ์ ์ ๊ฐ๋ฅ
-
spring-boot-starter-webflux
๋ผ๋ ์คํํฐ๊ฐ ์กด์ฌํ๋ฉฐ ๊ธฐ๋ณธ ๋ด์ฅ ์๋ฒ๋ ํฐ์บฃ์ด ์๋ Netty -
WebFlux์ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ Mono, Flux ๊ฐ์ ๋ฆฌ์กํฐ๋ธ ํ์ ์ ์ธ์๋ก ๋ฐ๊ฑฐ๋ ๋ฐํ, RxJava ํ์ ๋ ์ฒ๋ฆฌ ๊ฐ๋ฅ
-
Spring MVC์์๋ ๋ฆฌ์กํฐ๋ธ ํ์ ์ด ์ฌ์ฉ ๊ฐ๋ฅํ์ง๋ง ์ง์ ์ผ๋ก ๋ฆฌ์กํฐ๋ธํ๊ฒ ๋์ํ์ง๋ ์์
-
๊ธฐ์กด RestController
@GetMapping("/recent") public Iterable<Taco> recentTacos() { PageRequest page = PageRequest.of( 0, 12, Sort.by("createdAt").descending()); return tacoRepo.findAll(page).getContent(); }
-
๋ฆฌ์กํฐ๋ธ ํ์ ์ฌ์ฉ
@GetMapping("/recent") public Flux<Taco> recentTacos() { return tacoRepo.findAll().take(12); } // TacoRepository public interface TacoRepository extends ReactiveCrudRepository<Taco, Long> {}
- ๋ ํฌ์งํฐ๋ฆฌ์์ ๋ฆฌ์กํฐ๋ธ ํ์
์ ๋ฐ์ ๋
subscribe()
๋ ํ๋ ์์ํฌ์์ ์์์ ํธ์ถ
- ๋ ํฌ์งํฐ๋ฆฌ์์ ๋ฆฌ์กํฐ๋ธ ํ์
์ ๋ฐ์ ๋
-
RxJava ํ์ ์ฌ์ฉ
@GetMapping("/recent") public Observable<Taco> recentTacos() { return tacoService.getRecentTacos(); }
-
๋ฆฌ์กํฐ๋ธํ ์ ๋ ฅ ์ฒ๋ฆฌ
@PostMapping(consumes="application/json") @ResponseStatus(HttpStatus.CREATED) // ๋ฉ์๋ ์ง์ ์ ๋ธ๋กํน(์์ฒญ ํ์ด๋ก๋๊ฐ ๋ถ์๋์ด Taco ๊ฐ์ฒด๋ฅผ ์์ฑํด์ผ ์ฌ์ฉ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ) public Taco postTaco(@RequestBody Taco taco) { // ๋ค๋ฅธ ๋ฉ์๋๋ฅผ ํธ์ถ์ ๋ธ๋กํน(๋ค๋ฅธ ๋ฉ์๋๊ฐ ๋๋์ผ์ง๋ง ๋ค์ postTaco๊ฐ ์คํ์ด ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ) return tacoRepo.save(taco); }
- ์ด ๋๋ฒ์ ๋ธ๋กํน์ด ๊ฑธ๋ฆฌ๋ฏ๋ก ๋ฆฌ์กํฐ๋ธํ์ง ์์ผ๋ฏ๋ก ๋ค์๊ณผ ๊ฐ์ด ๋ฆฌ์กํฐ๋ธํ๊ฒ ๋ง๋ค ์ ์์
@PostMapping(consumes="application/json") @ResponseStatus(HttpStatus.CREATED) public Mono<Taco> postTaco(@RequestBody Mono<Taco> taco) { return tacoRepo.saveAll(tacoMono).next(); }
-
์ ๋ ธํ ์ด์ ์ ๊ฒฝ์ฐ ์ ๋ ธํ ์ด์ ์์ฒด๋ '๋ฌด์'์ ์ ์ํ๊ณ ์์ง๋ง '์ด๋ป๊ฒ'๋ ํ๋ ์์ํฌ ๋ด๋ถ์์ ์ ์ํ๋ฏ๋ก ๊ดด๋ฆฌ๊ฐ ์กด์ฌ
๋ฐ๋ผ์ ๋๋ฒ๊น ๋ ํ๋ฆ(์ค๋จ์ ์ ์ค์ ํ ์ ์๊ธฐ ๋๋ฌธ)
๋ํ, ์คํ๋ง์ด ์ฒ์์ธ ์ฌ๋๋ค์๊ฒ๋ ์ ๋ ธํ ์ด์ ๊ธฐ๋ฐ์ ํ๋ก๊ทธ๋๋ฐ์ ๊ธฐ๋ณธ ํ๋ก๊ทธ๋๋ฐ๊ณผ๋ ์ฌ๋ญ ๋ค๋ฆ -
๋ฐ๋ผ์, ์คํ๋ง 5์์๋ ์๋ก์ด ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ์ด ์๊ฐ
์๋ก์ด ํ๋ก๊ทธ๋๋ฐ ๋ชจ๋ธ์ ํ๋ ์์ํฌ๋ณด๋ค๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํํ๋ก ์ฌ์ฉ, ์ ๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ง ์๊ณ ์์ฒญ์ ํธ๋ค๋ฌ์ ๋งคํ
API ์์ฑ์์๋ 4๊ฐ์ง ๊ธฐ๋ณธ ํ์ ์ด ์๋ฐ- RequestPredicate : ์ฒ๋ฆฌ๋ ์์ฒญ์ ์ข ๋ฅ ์ ์ธ
- RouterFunction : ์ผ์นํ๋ ์์ฒญ์ด ์ด๋ป๊ฒ ํธ๋ค๋ฌ์๊ฒ ์ ๋ฌ๋์ด์ผ ํ๋์ง๋ฅผ ์ ์ธ
- ServerRequest : HTTP ์์ฒญ ์๋ฏธ, ํค๋์ ๋ชธ์ฒด ์ ๋ณด ์ฌ์ฉ ๊ฐ๋ฅ
- ServerResponse : HTTP ์๋ต ์๋ฏธ, ํค๋์ ๋ชธ์ฒด ์ ๋ณด ํฌํจ
@Configuration public class RouterFunctionConfig { @Bean public RouterFunction<?> helloRouterFun() { return route( GET("/hello"), // RequestPredicate ๊ฐ์ฒด request -> ok().body(just("Hello!"), String.class) // ์์ฒญ ์ฒ๋ฆฌ ํจ์ ); } }
- ์ฌ๋ฌ๊ฐ์ ์์ฒญ์ ์ฒ๋ฆฌํ ๋๋
addRoute()
๋ฉ์๋๋ฅผ ํตํด ์ฒด์ด๋์ด ๊ฐ๋ฅ
- WebTestClient๋ผ๋ ํ ์คํธ ์ ํธ๋ฆฌํฐ๊ฐ ์กด์ฌ
- GET, POST, PUT, PATCH, DELETE, HEAD ์์ฒญ ๊ฐ๋ฅ
- ์คํ ์ค์ด ์๋ฒ๋ก๋ ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๋ฉฐ,
@RunWith
์@SpringBootTest
์ ๋ ธํ ์ด์ ์ ์ง์ ํ๋ ๊ฒ๋ถํฐ ์์
์์์์๋WebEnvironment.RANDOM_PORT
๋ฅผ ์ฌ์ฉํ์ฌ ํฌํธ๋ฅผ ์ง์ ํ์ง๋ง, ์ด๋ ํฌํธ์ ํฌ๋์ฌ ์ํ์ด ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก ๊ถ์ฅ X
- REST API์ ์์ฒญ์ ํ๋ ๋ฐฉ์์ผ๋ก ๊ธฐ์กด์๋ RestTemplate์ ์ฌ์ฉํ์ง๋ง ์ด๋ ๋ฆฌ์กํฐ๋ธ ํ์ ์ ์ฒ๋ฆฌ ํ ์ ์์
- RestTemplate์ ๋์์ผ๋ก WebClient๋ฅผ ์ ๊ณต, ์ ์ก๊ณผ ์์ ๋ชจ๋ ํจ
- WebClient์ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์
- WebClient ์ธ์คํด์ค ์์ฑ(ํน์ ๋น ์ฃผ์ )
- ์์ฒญ์ ์ ์กํ HTTP ๋ฉ์๋ ์ง์
- ์์ฒญ์ ํ์ํ URI์ ํค๋ ์ง์
- ์์ฒญ ์ ์ถ(๋น๋ ๋ฐฉ์์ ์ธํฐํ์ด์ค ์ฌ์ฉ)
- ์๋ต ์๋น
Mono<Ingredient> ingredient = WebClient.create() //์ธ์คํด์ค ์์ฑ
.get() // GET ์์ฒญ(๋ฉ์๋ ์ง์ )
.uri("http://localhost:8080/ingredients/{id}", ingredientId) // URI ์ง์
.retrieve() // ์์ฒญ ์คํ
.bodyToMono(Ingredient.class); // ์๋ต์ ๋ฐ๋๋ฅผ Mono๋ก ์ถ์ถ
// ์์ฒญ์ด ๋ง๋ฌด๋ฆฌ ๋๊ธฐ ์ํด์ ๊ตฌ๋
์ด ๋์ด์ผํ๋ฏ๋ก subscribe ๋ฉ์๋๋ ๋ง์ง๋ง์ ํธ์ถ
// ์ถ๊ฐ์ ์ธ ์คํผ๋ ์ด์
(filter, map...)์ subscribe ์ด์ ์ ํธ์ถ
ingredient
.map(i -> {
...
})
.subscribe(i -> {
...
});
// ๋ฑ๋ก
@Bean
public WebClient webClient() {
return WebClient.create("http://localhost:8080");
}
// ์ฌ์ฉ
@Autowired
WebClient webClient;
public Mono<Ingredient> getIngredientById(String id) {
Mono<Ingredient> ingredient = webClient
.get()
.uri("/ingredients/{id}", id)
.retrieve()
.bodyToMono(Ingredient.class);
ingredient.subscribe(i -> {...});
}
- ๋ค์๊ณผ ๊ฐ์ด ๊ธฐ๋ณธ์ ์ผ๋ก ํธ์ถํ๋ URI์ ๋น์ผ๋ก ๋ฑ๋กํ๊ณ ์ฌ์ฉํ ์ ์์
- ๋ฐ์ดํฐ๋ฅผ ์์ ํ๋ ๊ฒ๊ณผ ๋ง์ด ๋ค๋ฅด์ง ์์
body()
๋ฉ์๋๋ฅผ ํตํด ๋ณด๋ด๊ณ ์ถ์ ๋ฆฌ์กํฐ๋ธ ๊ฐ์ฒด๋ฅผ ๋ฃ๊ธฐ๋ง ํ๋ฉด ๋จ
Mono<Ingredient> ingredientMono = ...;
Mono<Ingredient> result = webClinet
.post()
.uri("/ingredients")
.body(ingredientMono, Ingredient.class)
.retrieve()
.bodyToMono(Ingredient.class);
result.subscribe(i -> {...});
- ๋ง์ฝ ๋ฆฌ์กํฐ๋ธ ํ์
์ด ์๋ ์ผ๋ฐ ๋๋ฉ์ธ ๊ฐ์ฒด์ผ ๊ฒฝ์ฐ
syncBody()
๋ฉ์๋๋ฅผ ์ฌ์ฉ ๊ฐ๋ฅ
- DELETE ํน์ PUT๊ณผ ๊ฐ์ ๋ฉ์๋๋ ์๋ต ํ์ด๋ก๋๋ฅผ ๊ฐ์ง๊ณ ์์ง ์๊ธฐ ๋๋ฌธ์
bodyToOOO()
์๋Void
ํ์ ์ ๋ฃ์ด์ผ ํ๋ฉฐ,subscribe()
๋ก ๊ตฌ๋
Mono<Void> result = webClinet
.delete
.uri("/ingredients/{id}", id)
.syncBody(ingredient)
.retrieve()
.bodyToMono(Void.class)
.subscribe();
- ์๋ฌ ์ฒ๋ฆฌ์์๋
onStatus()
๋ฉ์๋๋ฅผ ํธ์ถ, HTTP ์ํ ์ฝ๋๋ฅผ ์ง์ ๊ฐ๋ฅ onStatus()
๋ฉ์๋๋ ์ฒ๋ฆฌํด์ผ ํ HTTP ์ํ์ ์ผ์น์ํค๋๋ฐ ์ฌ์ฉ๋๋ ์กฐ๊ฑด ํจ์,Mono<Throwable>
์ ๋ฐํํ๋ ํจ์ ๋ฅผ ์ธ์๋ก ๊ฐ์ง
Mono<Ingredient> ingredient = WebClient
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.onStatus(status -> status == HttpStatus.NOT_FOUND,
response -> Mono.just(new UnkownIngredientException()))
.bodyToMono(Ingredient.class);
-
subscribe()
๋ฉ์๋์์๋ ์ด๋ฅผ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅingredientMono.subscribe( ingredient -> { // ์์์ ๋ฐ์ดํฐ ์ฒ๋ฆฌ }, error -> { // ์๋ฌ ์ฒ๋ฆฌ } );
-
ID์ ๋์ผํ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์์ ์์๋ 1๋ฒ์งธ ์ธ์์ ์๋ ํจ์ ์คํ, ์ฐพ์ง ๋ชปํ๋ฉด 404 ์ํ ์ฝ๋๋ฅผ ๊ฐ๊ฒ ๋๊ณ 2๋ฒ์งธ ์ธ์๋ก ์ ๋ฌ๋ ํจ์๊ฐ ์คํ๋์ด ๊ธฐ๋ณธ์ ์ผ๋ก
WebClientResponseException
์ ๋ฐ์ -
WebClientResponseException
์ ๊ฒฝ์ฐ ์ด๋ ํ ์์ธ์ผ๋ก ๋ฐ์ํ๋์ง๋ฅผ ๋ชจ๋ฆ -
๋ฐ๋ผ์
onStatus()
์ฌ์ฉํ๋๊ฒ์ด ์ข์
retrieve()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์์ฒญ์ ์ ์ก,retrieve()
๋ฉ์๋๋ResponseSpec
ํ์ ์ ๊ฐ์ฒด๋ฅผ ๋ฐํ
์ด๋ฅผ ํตํดonStatus()
,bodyToFlux()
๋ฑ๊ณผ ๊ฐ์ ์ฌ๋ฌ๊ฐ์ง ๋ฉ์๋๋ค์ ํธ์ถ- ๊ฐ๋จํ ์ํฉ์์๋
retrieve()
๊ฐ ์ข์ง๋ง ์๋ต์ ํค๋๋ ์ฟ ํค๊ฐ์ ์ฌ์ฉํ์ง ๋ชปํจ - ์ด๋ด ๋๋
exchange()
๋ฅผ ํธ์ถํ๋ฉฐ ์ด๋ClientResponse
ํ์ ์Mono
๋ฅผ ๋ฐํ - ๋ฐ๋ผ์ ๋ฆฌ์กํฐ๋ธ ์คํผ๋ ์ด์ ์ ๋ชจ๋ ์ ์ฉ ๊ฐ๋ฅํ๋ฉฐ ์๋ต์ ๋ชจ๋ ๋ถ๋ถ(์ฟ ํค, ํค๋, ํ์ด๋ก๋ ๋ฑ) ์ฌ์ฉ ๊ฐ๋ฅ
- ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ฐ์ ์น ๋ณด์ ๋ชจ๋ธ์ ๊ฒฝ์ฐ ์๋ธ๋ฆฟ ํํฐ๋ฅผ ์ค์ฌ์ผ๋ก ๋ง๋ค์ด์ง
- ์๋ธ๋ฆฟ ์ปจํ ์ด๋๋ฅผ ์ฌ์ฉํ์ง ์๋ WebFlux์์๋ ํด๋น ๋ฐฉ๋ฒ์ ๊ณค๋(์๋ธ๋ฆฟ์ด ๊ฐ์ ๋๋ค๋ ๋ณด์ฅ X)
- ๋์ WebFilter๊ฐ ์ด๋ฅผ ํด์ค
- ์ด๋ ์คํ๋ง ์ํ๋ฆฌํฐ์ ๊ฑฐ์ ๋ค๋ฅผ๋ฐ๊ฐ ์์ผ๋ฉฐ, ๊ฐ์ ์คํํฐ ์์ ์กด์ฌ
-
๊ธฐ์กด ์ํ๋ฆฌํฐ ์ค์
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/design","/orders").hasRole("USER") .antMatchers("/", "/**").permitAll(); } }
@EnableWebSecurity
์ ๋ ธํ ์ด์ ์ฌ์ฉWebSecurityConfigurerAdapter
์์ ๋ฐconfigure()
์ค๋ฒ๋ผ์ด๋ฉ
-
WebFlux์ ์ํ๋ฆฌํฐ ์ค์
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain( ServerHttpSecurity http ) { return http.authorizeExchange() .pathMatchers("/design","/orders").hasAuthority("USER") .anyExchange().permitAll() .and() .build(); } }
@EnableWebFluxSecurity
์ ๋ ธํ ์ด์ ์ฌ์ฉSecurityWebFilterChain
ํ์ ์ ๋น์ ์ ์ธServerHttpSecurity
๊ฐ์ฒด ์ฌ์ฉ- ๋ฐ๋์
build()
๋ฉ์๋๋ฅผ ํธ์ถํ์ฌSecurityWebFilterChain
ํ์ ์ผ๋ก ์กฐ๋ฆฝํ๊ณ ๋ฐํ
-
๊ธฐ์กด ์ํ๋ฆฌํฐ ์ค์ ์ ๊ฒฝ์ฐ
WebSecurityConfigurerAdapter
์ ๋ค๋ฅธconfigure()
๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉ ํ์ฌUserDetails
๊ฐ์ฒด๋ก ์ ์ํ๋ ์ธ์ฆ ๋ก์ง ๊ตฌ์ฑ -
WebFlux์์๋
ReactiveUserDetailsService
์ด๋ ๋น์ ์ ์ธ, ํ๋์ ๋ฉ์๋๋ง ๊ตฌํํ๋ฉด ์ฌ์ฉ ๊ฐ๋ฅ@Service public ReactiveUserDetailService userDetailsService( UserRepository userRepo) { // ๋ฆฌ์กํฐ๋ธ ์คํ๋ง ๋ฐ์ดํฐ ๋ ํฌ์งํฐ๋ฆฌ return new ReactiveUserDetailsService() { @Override public Mono<UserDetails> findByUserName(String name) { return userRepo.findByUsername(name) .map(user -> { return user.toUserDetails(); }) } } }