-
[Spring In Action] 11. 리액티브 API 개발하기Programming Language/Spring 2023. 3. 24. 13:29반응형
Chapter 11. REST API를 리액티브하게 사용하기
스프링 5가
RestTemplate
의 리액티브 대안으로 WebClinet를 제공WebClient
는 외부 API로 요청을 할 때 리액티브 타입의 전송과 수신 모두를 한다.리소스 얻기(GET)
RestTemplate의 경우는 getForObject()
Mono<Ingredient> ingredient = WebClient.create() .get() .uri("http://localhost:8080/ingredients/{id}", ingredientId) .retrieve() .bodyToMono(Ingredient.class); ingredient.subscribe(i -> { ... })
컬렉션에 저장된 값들을 반환하는 요청
Flux<Ingredient> ingredients = WebClient.create() .get() .uri("http://localhost:8080/ingredients") .retrieve() .bodyToFlux(Ingredient.class); ingredients.subscribe(i -> { ... })
차이점이라면 bodyToMono()를 사용해서 응답 몸체를 추찰하는 대신 bodyToFlux()를 사용해서 Flux로 추출하는 것
기본 URI로 요청하기
@Bean public WebClient webClient() { return WebClient.create("http://locahost:8080"); }
@Autowired WebClient webClient; public Mono<Ingredient> getIngredientById(String ingredientId) { Mono<Ingredient> ingredient = webClient .get() .uri("/ingredients/{id}", ingredientId) .retrieve() .bodyToMono(Ingredient.class); ingredient.subscribe(i -> { ... }) }
리소스 전송하기
WebClent로 데이터를 전송하는 것은 데이터 수신과 그리 다르지 않다.
get() 대신 post() 메서드를 사용하고 body()를 호출하여 Mono를 사용해서 해당 요청 몸체에 넣는다는 것만 지정하면 된다.Mono<Ingredient> ingredientMono = ...; Mono<Ingredient> result = webClient .post() .uri("/ingredients") .body(ingredientMono, Ingredient.class) .retrieve() .bodyToMono(Ingredient.class); result.subscribe(i -> { ... })
만일 전송할 Mono나 Flux가 없는 대신 도메인 객체가 있다면 syncBody()를 사용할 수 있다.
POST 요청 대신 PUT 요청으로 Ingredient 객체를 변경하고 싶다면 post() 대신 put()을 호출하고 이에 맞춰 URI 경로를 조절하면 된다.Mono<Void> result = webClient .put() .uri("/ingredients/{id}", ingredient.getId()) .syncBody(ingredient) .retrieve() .bodyToMono(Void.class) .subscribe();
리소스 삭제하기
WebClient는 또한 delete() 메서드를 통해 리소스의 삭제를 허용한다.
Mono<Void> result = webClient .delete() .uri("/ingredients/{id}, ingredientId) .retrieve() .bodyToMono(Void.class) .subscribe();
에러 처리하기
Mono<Ingredient> ingredientMono = webClient .get() .uri("http://localhost:8080/ingredients/{id}", ingredientId) .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> Mono.just(new UnknownIngredientException())) .bodyToMono(Ingredient.class);
요청 교환하기
지금까지 WebClient를 사용할 때는 retrieve() 메서드를 사용해서 요청의 전송을 나타냈다.
이때 retrieve() 메서드는 ResponseSpec 타입의 객체를 반환하였으며, 이 객체를 통해서 onStatus(), bodyToFlux(), bodyToMono()와 같은 메서드를 호출하여 응답을 처리할 수 있었다. 그러나 이경우 몇 가지 면에서 제한된다.예를 들어 응답의 헤더나 쿠키 값을 사용할 필요가 있을 때는 ResponseSpec으로 처리할 수 없다.
ResponseSpec이 기대에 미치지 못할 때는 retrieve() 대신 exchage()를 호출할 수 있다.
exchange() 메서드는 ClientReponse 타입의 Mono를 반환한다. ClientResponse 타입은 리액티브 오퍼레이션을 적용할 수 있고, 응답의 모든 부분(페이로드, 헤더, 쿠키 등)에서 데이터를 사용할 수 있다.
다음 코드에서는 WebClient와 exchange()를 사용해서 특정 ID를 갖는 하나의 식자재를 가져온다.
Mono<Ingredient> ingredientMono = webClient .get() .uri("http://localhost:8080/ingredients/{id}", ingredientId) .exchange() .flatMap(cr -> cr.bodyToMono(Ingredient.class));
이 코드는 retrieve()를 사용한 다음 코드와 거의 같다.
Mono<Ingredient> ingredientMono = webClient .get() .uri("http://localhost:8080/ingredients/{id}", ingredientId) .retrieve() .botyToMono(Ingredient.class);
요청의 응답에 true 값(해당 식자재가 사용 가능하지 않다는 것을 나타냄)을 갖는 X_UNAVAILABLE이라는 이름의 헤더가 포함될 수 있다고 하자.
그리고 X_UNAVAILABLE 헤더가 존재한다면 결과 Mono는 빈 것(아무것도 반환하지 않는)이어야 한다고 가정해보자.
이경우 다음과 같이 또다른 flatMap() 호출을 추가하면 된다.
Mono<Ingredient> ingredientMono = webClient .get() .uri("http://localhost:8080/ingredients/{id}", ingredientId) .exchange() .flatMap(cr -> { if (cr.headers().header("X_UNAVAILABLE").contains("true")) { return Mono.empty(); } return Mono.just(cr); }) .flatMap(cr -> cr.bodyToMono(Ingredient.class));
리액티브 웹 API 보안
이제까지 스프링 시큐리티의 웹 보안 모델은 서블릿 필터를 중심으로 만들어졌다.
스프링 WebFlux로 웹 애플리케이션을 작성할 때는 서블릿이 개입된다는 보장이 없다.
그러나 5.0.0 버전부터 스프링 시큐리티는 서블릿 기반의 스프링 MVC와 리액티브 스프링 WebFlux 애플리케이션 모두의 보안에 사용될 수 있다.
스프링의 WebFilter가 이 일을 해준다. WebFilter는 서블릿 API에 의존하지 않는 스프링 특유의 서블릿 필터 같은 것이다.스프링 시큐리티는 스프링 MVC와 동일한 스프링 부트 보안 스타터를 사용한다.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
리액티브 웹 보안 구성하기
리액티브가 아닌 스프링 MVC 웹 애플리케이션의 보안 구성하기
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/design", "/orders").hasAuthority("USER") .antMatchers("/**").permitAll(); } }
다음은 이것과 동일한 구성을 리액티브 스프링 WebFlux 애플리케이션에서는 어떻게 하는지 알아보자.
@Configuration @EnableWebFluxSecurity public class SecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .authorizeExchange() .pathMatchers("/design", "/orders").hasAuthority("USER") .anyExchange().permitAll() .and() .build(); } }
리액티브 사용자 명세 서비스 구성하기
WebSecurityConfigurerAdapter의 서브 클래스로 구성 클래스를 작성할 때는 하나의 configure() 메서드를 오버라이딩하여 웹 보안 규칙을 선언하며, 또다른 configure() 메서드를 오버라이딩하여 UserDetails 객체로 정의하는 인증 로직을 구성한다.
@Autowired UserRepository userRepo; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepo.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(username " + not found"); } return user.toUserDetails(); } }); }
그러나 리액티브 보안 구성에서는 configure() 메서드를 오버라이딩하지 않고 대신에 ReactiveUserDetailsService 빈을 선언한다. 이것은 UserDetailsService의 리액티브 버전이며 UserDetailsService처럼 하나의 메서드만 구현하면 된다. 특히 findByUsername() 메서드는 UserDetails 객체 대신 Mono<UserDetails>를 반환한다.
@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(); }); }); }; }
요약
- 스프링 WebFlux는 리액티브 웹 프레임워크를 제공한다. 이 프레임워크의 프로그래밍 모델은 스프링 MVC가 많이 반영되었다. 심지어는 애노테이션도 많은 것을 공유한다.
- 스프링 5는 또한 스프링 WebFlux의 대안으로 함수형 프로그래밍 모델을 제공한다.
- 리액티브 컨트롤러는 WebTestClient를 사용해서 테스트할 수 있다.
- 클라이언트 측에는 스프링 5가 스프링 RestTemplate의 리액티브 버전인 WebClient를 제공한다.
- 스프링 시큐리티 5는 리액티브 보안을 지원하며, 이것의 프로그래밍 모델은 리액티브가 아닌 스프링 MVC 애플리케이션의 것과 크게 다르지 않다.
반응형'Programming Language > Spring' 카테고리의 다른 글
대용량 처리를 위한 MySQL 이해 (0) 2023.05.25 Microservice와 Spring Cloud 소개 (0) 2023.05.05 @Transaction(readOnly = true) (0) 2022.11.21 [Spring] 스프링 트랜잭션 (0) 2022.09.30 [Spring] @Bean vs @Component (0) 2022.09.28