ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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
Designed by Tistory.