본문 바로가기
Programming Language/Spring

[Spring In Action] 11. 리액티브 API 개발하기

by happy coding! 2023. 3. 24.
반응형

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

댓글