[Spring In Action] 11. 리액티브 API 개발하기
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 애플리케이션의 것과 크게 다르지 않다.