[스프링 인 액션] 11.리액티브 API 개발하기

김하영·2021년 8월 12일
0
post-custom-banner
  • 이 장에서 배우는 내용
    스프링 WebFlux 사용하기
    리액티브 컨트롤러와 클라이언트 작성하고 테스트하기
    REST API 소비하기
    리액티브 웹 애플리케이션의 보안

스프링 WebFlux는 스프링 MVC와 매우 유사하며 적용하기 쉽다.
스프링 REST API 생성에 관해 우리가 이미 알고 있는 것의 많은 부분을 활용할 수 있다.

11.1 스프링 WebFlux 사용하기

매 연결마다 하나의 스레드를 사용하는 스프링 MVC 같은 전형적인 서블릿 기반의 웹 프레임워크는 스레드 블로킹(차단)과 다중 스레드로 수행된다.
따라서 블로킹 웹 프레임워크는 요청량의 증가에 따른 확장이 사실상 어렵다.
게다가 처리가 느린 작업 스레드로 인해 훨씬 더 심각한 상황이 발생한다.
해당 작업 스레드가 풀로 반환되어 다른 요청 처리를 준비하는 데 더 많은 시간이 걸리기 때문이다.
상황에 따라서는 이 방식이 완벽히 받아들일 만하다.
실제로 이것은 대부분의 웹 애플리케이션 개발에 10년 넘게 사용된 방법이다. 그러나 시대가 바뀌고 있다.

비동기 웹 프레임워크

더 적은 수의 스레드(일반적으로 CPU 코어당 하나)로 더 높은 확장성을 성취한다.
이벤트 루핑(event loop) 이라는 기법을 적용한 이런 프레임워크는 한 스레드당 많은 요청을 처리할 수 있어서 한 연결당 소요 비용이 더 경제적이다.

비용이 드는 작업이 필요할 때 이벤트 루프는 해당 작업의 콜백(call back)을 등록하여 병행으로 수행되게 하고 다른 이벤트 처리로 넘어간다.

11.1.1 스프링 WebFlux 개요

스프링 5는 WebFlux라는 새로운 웹 프레임워크로 리액티브 웹 애플리케이션의 Flux는 스프링 MVC의 핵심 컴포넌트를 공유한다.
Spring MVC는 자바 서블릿 API의 상위 계층에 위치

WebFlux 특징

Spring WebFlux 는 서블릿 API와 연계되지 않는다.
서블릿 API가 제공하는 것과 동일한 기능의 리액티브 버전인 리액티브 HTTP API의 상위 계층에 위치한다.
Spring WebFlux는 서블릿 API에 연결되지 않으므로 실행하기 위해 서블릿 컨테이너를 필요로 하지 않는다.
대신에 블로킹이 없는 어떤 웹 컨테이너에서도 실행될 수 있으며 이에는 Netty, Undertow, 톰캣, Jetty 또는 다른 서블릿 3.1 이상의 컨테이너가 포함된다.
함수형 프로그래밍 패러다임으로 컨트롤러를 정의하는 대안 프로그래밍 모델을 나타낸다.

11.1.2 리액티브 컨트롤러 작성하기

6장에서 타코 클라우드의 REST API 컨트롤러 생성을 리액티브를 변경한다.
요청 처리 메서드를 가지고 있는 컨트롤러 - 도메인 타입(Taco, Order) 또는 도메인 타입의 컬렉션으로 입력과 출력을 처리한다.

Iterable은 리액티브 타입이 아니며 Iterable에는 어떤 리액티브 오퍼레이션도 적용할 수 없다.
프레임워크가 Iterable 타입을 리액티브 타입으로 사용하여 여러 스레드에 걸쳐 작업을 분할하게 할 수도 없다.
recentTacos()가 Flux 타입을 반환하게 하는 것이다.

RxJava 타입 사용하기

스프링 WebFlux를 사용할 때 Flux나 Mono와 같은 리액티브 타입이 자연스러운 선택이지만 Observable이나 Single과 같은 RxJava 타입을 사용할 수 있다.
WebFlux는 또한 Observable이나 리액터 Flux 타입의 대안으로 Flowable 타입을 반환할 수 있다.

리액티브하게 입력 처리하기

지금까지는 컨트롤러 메서드가 반환하는 리액티브 타입만 알아보았다.
그러나 스프링 WebFlux를 사용할 때 요청을 처리하는 핸들러 메서드의 입력으로도 Mono나 Flux를 받을 수 있다.

리퍼지터리의 save() 메서드의 블로킹되는 호출이 끝나고 복귀되어야 postTaco()가 끝나고 복귀할 수 있다는 것을 의미한다.
요청은 두 번 블로킹된다.
postTaco()로 진입할 때와 postTaco()의 내부에서다.

@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Taco postTaco(@RequestBody Taco taco) {
    return tacoRepo.save(taco);
}

그러나 postTaco()에 조금만 리액티브 코드를 적용하면 완전하게 블로킹되지 않는 요청 처리 메서드로 만들 수 있다.

saveAll() 메서드는 Mono나 Flux를 포함해서 리액티브 스트림의 publisher 인터페이스를 구현한 어떤 타입도 인자로 받을 수 있다.
saveAll() 메서드는 Flux를 반환한다.
그러나 postTaco()의 인자로 전달된 Mono를 saveAll()에서 인자로 받았으므로 saveAll이 반환하는 Flux가 하나의 Taco객체만 포함되어야 한다.

next()를 호출하여 Mono로 받을 수 있으며 이것을 postTaco가 반환한다.

Flux는 0,1 또는 다수의(무한일 수 있는) 데이터를 갖는 파이프라인을 나타낸다.
반면에 Mono는 하나의 데이터 항목만 갖는 데이터셋에 최적화된 리액티브 타입이다.

@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public Mono<Taco> postTaco(@RequestBody Mono<Taco> tacoMono) {
    return tacoRepo.saveAll(tacoMono).next();
}

11.2 함수형 요청 핸들러 정의하기

스프링 MVC 애노테이션 기반 프로그래밍 모델의 몇가지 단점

  1. 애노테이션 기반 프로그래밍이건 애노테이션이 무엇을 하는지와 어떻게 하는지를 정의하는 데 괴리가 있다.
    이로인해 프로그래밍 모델을 커스터마이징 하거나 확장할 때 복잡해진다. 변경을 하려면 애노테이션 외부에 있는 코드로 작업을 해야하기 때문이다.

  2. 디버깅 하기 어렵다. 왜냐하면 애노테이션에 breakpoint(중단점)을 설정할 수 없기 때문이다.

그.래.서

스프링 5에는 리액티브 API를 정의하기 위한 새로운 함수형 프로그래밍 모델 소개

새로운 프로그래밍 모델은 프레임워크보다는 라이브러리 형태로 사용되므로 애노테이션을 사용하지 않고 요청을 핸들러 코드에 연관시킨다.
스프링의 함수형 프로그래밍 모델을 사용한 API의 작성에는 다음 네가지 기본 타입이 수반된다.

  • RequestPredicate: 처리될 요청의 종류를 선언한다.
  • RouterFunction: 일치하는 요청이 어떻게 핸들러에게 전달되어야 하는지를 선언한다.
  • ServerRequest: HTTP 요청을 나타내며 헤더와 몸체 정보를 사용할 수 있다.
  • ServerResponse: HTTP 응답을 나타내며 헤더와 몸체 정보를 포함한다.

참고로 helper class를 static import하여 함수형 타입을 생성하는데 사용한다.

import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
import static reactor.core.publisher.Mono.just;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

11.3 리액티브 컨트롤러 테스트하기

WebTestClient는 스프링 WebFlux를 사용하는 리액티브 컨트롤러의 테스트를 쉽게 작성하게 해주는 새로운 유틸리티이다.

11.3.3 실행 중인 서버로 테스트하기

지금까지 사용했던 테스트는 모의 스프링 WebFlux 프레임워크를 사용했으므로 실제 서버가 필요 없었다.
Netty나 톰캣과 같은 서버 환경에서 리퍼지토리나 다른 의존성 모듈을 사용해서 WebFlux 컨트롤러를 테스트할 필요가 있을 수 있다.

WebTestClient의 통합 테스트를 작성하기 위해 @RunWith(SpringRunner.class), @SpringBootTest 애노테이션 작성한다.
무작위로 선택된 포트로 실행 서버가 리스닝하도록 스프링에 요청한다.

@Autowired를 지정하여 WebTestClient를 테스트 클래스로 자동 연결한다.
테스트 메소드에서 WebTestClient 인스턴스를 더 이상 생성할 필요가 없고, 요청할 때 완전한 URL을 지정할 필요도 없다.
태스트 서버가 어떤 포트에서 실행 중인지 알 수 있게 WebTestClient가 설정되기 때문이다.

자동 연결되는 인스턴스를 사용하기 때문에 WebTestClient 인스턴스를 생성할 필요가 없다.
스프링이 DesignTacoController의 인스턴스를 생성하고 실제 TacoRespository를 주입하기 때문에 더 이상 모의 TacoRespository도 필요 없다.

package tacos.web;


import java.io.IOException;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient  // Could not autowire. No beans of 'WebTestClient' type found
public class DesignTacoControllerWebTest {

    @Autowired
    private WebTestClient testClient;

    @Test
    public void shouldReturnRecentTacos() throws IOException {
        testClient.get().uri("/design/recent")
                .accept(MediaType.APPLICATION_JSON).exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$[?(@.id == 'TACO1')].name")
                .isEqualTo("Carnivore")
                .jsonPath("$[?(@.id == 'TACO2')].name")
                .isEqualTo("Bovine Bounty")
                .jsonPath("$[?(@.id == 'TACO3')].name")
                .isEqualTo("Veg-Out");
    }

}

11.4 REST API를 리액티브하게 사용하기

스프링 리액티브 웹의 클라이언트 측면에 관심을 돌려서 Mono나 Flux같은 리액티브 타입을 사용하는 REST 클라이언트를 WebClient가 어떻게 제공하는지 알아본다.

7장에서는 RestTemplate를 사용해서 타코 클라우드 API의 클라이언트 요청을 하였다.
스프링 3.0에서 소개되었던 RestTemplate은 이제 구세대가 되었다. 그 당시에는 많은 애플리케이션이 무수한 요청에 RestTemplate을 사용했다.
그러나 RestTemplate이 제공하는 모든 메서드는 리액티브가 아닌 도메인 타입이나 컬렉션을 처리한다.

따라서 리액티브 방식으로 응답 데이터를 사용하고자 한다면 이것을 Flux나 Mono 타입으로 래핑해야 한다.

따라서 RestTemplate을 리액티브 타입으로 사용하는 방법으로 스프링 5가 RestTemplate의 리액티브 대안으로 WebClient를 제공한다.

WebClient는 외부 API로 요청을 할 때 리액티브 타입의 전송과 수신 모두를 한다.
RestTemplate는 다수의 메서드로 서로 다른 종류의 요청을 처리하는 대신 WebClient는 요청을 나타내고 전송하게 해주는 빌더 방식의 인터페이스 사용한다.

WebClient를 사용하는 일반적인 패턴

WebClient의 인스턴스를 생성한다 (또는 WebClient 빈을 주입한다.)

@Bean
public WebClient webClient() {
   return WebClient.create("http://localhost:8080");
}

11.4.1 리소스 얻기(GET)

타코 클라우드 API로부터 식자재를 나타내는 특정 Ingredient 객체를 이것의 ID를 사용해서 가져와야 한다고 해보자.
RestTemplate의 경우는 getForObject() 메서드를 사용할 수 있다.
그러나 WebClient를 사용할 때는 요청을 생성하고 응답을 받은 다음에 Ingredient 객체를 발행하는 Mono를 추출한다.

  1. create() 메서드로 새로운 WebClient 인스턴스를 생성
  2. get()과 url를 사용해서 GET 요청을 정의
  3. retrieve() 메서드는 해당 요청을 실행
  4. 마지막으로 bodyToMono() 호출에서는 응답 몸체의 페이로드를 Mono로 추출한다.
    따라서 이 코드는 다음에는 계속해서 Mono의 다른 오퍼레이션들을 연쇄 호출할 수 있다.

bodyToMono()로부터 반환되는 Mono에 추가로 오퍼레이션을 적용하려면 해당 요청이 전송되기 전에 구독을 해야 한다.
다수의 항목을 가져오는 것은 단일 항목을 요청하는 것과 동일하다.

bodyToFlux()를 사용해서 Flux로 추출할 수도 있다.

Mono<Ingredient> ingredient = WebClient.create()
.get()
.uri("http://localhost:8080/ingredients/{id}", ingredientId)
.retrieve()
.bodyToMono(Ingredient.class);

ingredient.subscribe(i -> {...})

11.4.2 리소스 전송하기

WebClient로 데이터를 전송하는 것은 데이터 수신과 크게 다르지 않다.
get() 대신 post() 메소드를 사용하고 body()를 호출하여 Mono를 사용해서 해당 요청 몸체에 넣는 다는 것만 지정하면 된다.

Mono<Ingredient> ingredientMono = ...;
body(ingredientMono, Ingredient.class)

11.5 리액티브 웹 API 보안

스프링 시큐리티의 웹 보안 모델은 서블릿 필터를 중심으로 만들어졌다.

만일 요청자가 올바른 권한을 갖고 있는지 확인하기 위해 서블릿 기반 웹 프레임워크의 요청 바운드를(클라이언트의 요청을 서블릿이 받기 전에) 가로채야 한다면 서블릿 필터가 확실한 선택이나 스프링 WebFlux에서는 불가능하다.

스프링 WebFlux로 웹 애플리케이션을 작성할 때는서블릿이 개입된다는 보장이 없다.
실제로 리액티브 웹 애플리케이션은 Netty나 일부 다른 non-서블릿 서버에 구축될 가능성이 많다.

그렇다면 서블릿 필터 기반의 스프링 시큐리티는 스프링 WebFlux 애플리케이션 보안에 사용될 수 없는 것일까?

스프링 5.0.0 버전부터 스프링 시큐리티는 서블릿 기반의 스프링 MVC와 리액티브 스프링 WebFlux 애플리케이션 모두의 보안에 사용될 수 있다.

스프링 WebFlux 애플리케이션의 스프링 시큐리티 구성하기

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public SecurityWebFilterChain  securityWebFilterChain(ServerHttpSecurity http) {
        return http
                .authorizeExchange()
                .pathMatchers("/design", "orders").hasAuthority("USER")
                .anyExchange().permitAll()
                .and()
                .build();
    }

요약

  • 스프링 WebFlux는 리엑티브 웹 프레임워크를 제공한다.
    이 프레임워크의 프로그래밍 모델은 스프링 MVC가 많이 반영되었다. 심지어는 애노테이션도 많은 것을 공유한다.

  • 스프링 5는 또한 스프링 WebFlux의 대안으로 함수형 프로그래밍 모델을 제공한다.

  • 리액티브 컨트롤러는 WebTestClient를 사용해서 테스트할 수 있다.

  • 클라이언트 측에는 스프링 5가 스프링 RestTemplate의 리액티브 버전인 WebClient를 제공한다.

  • 스프링 시큐리티 5는 리액티브 보안을 지원하며, 이것이 프로그래밍 모델은 리액티브가 아닌 스프링 MVC 애플리케이션의 것과 크게 다르지 않다.

profile
Back-end Developer
post-custom-banner

0개의 댓글