# Table of Contents

# 동기-블로킹 모델

일반적으로 동기-블로킹 모델은 멀티 스레드로 동작한다. 멀티 스레드 모델은 하나의 요청을 하나의 스레드가 처리하며, 이를 Thread per request라고 한다.

동기-블로킹 모델에서는 네트워크 통신, 입출력 작업을 수행하면 스레드가 자원을 점유한 채 대기상태로 진입한다. 하드웨어 관점에서 보면 CPU는 네트워크 통신이나 입출력의 처리를 IO Controller에게 위임하며, CPU는 다른 스레드를 처리한다.

이 과정에서 많은 Context Switching이 발생한다. 또한 멀티 스레드 모델에서는 요청이 많을 수록 스레드가 많아지며, 이에 대한 부하도 커진다.

# 비동기-논블로킹 모델

대표적인 비동기-논블로킹 모델인 Node.js를 먼저 살펴보자. Node.js는 비동기-논블로킹, 싱글 스레드로 동작한다. 이 모델에서는 이벤트 루프라는 스레드가 무한 루프를 돌며 사용자의 모든 요청을 순차적으로 처리한다.

만약 네트워크 통신, 입출력 처리가 필요하다면 IO Controller에게 이를 위임한다. 그리고 이벤트 루프는 대기 상태로 진입하지 않고 다른 요청을 계속 처리한다. 이 과정에서 네트워크 통신, 입출력 처리가 끝나는 시점을 알기 위해 Callback을 등록하며, Callback은 Callback Queue라는 공간에 저장된다.

네트워크 통신, 입출력이 종료되면 IO Controller는 이벤트 루프에 이를 알리게 되며, 이벤트 루프는 해당 콜백을 가져와 실행하게 된다.

콜백에는 네트워크 통신, 입출력이 끝났을 때 실행할 코드를 구현하며, 보통 다음과 같이 네트워크 통신 또는 입출력을 실행하는 구문의 마지막 인자에 람다식 형태로 콜백을 전달한다.

Long userId = 1L;

getUserDataFromNetwork(userId, (userData) -> {
    // 네트워크 통신 또는 입출력을 실행하는 구문
});

Spring WebFlux 또한 비동기-논블로킹 모델이지만 싱글 스레드는 아니다. Spring WebFlux는 CPU 코어 수 만큼의 스레드를 사용한다.

비동기-논블로킹 모델은 적은 수의 스레드로 모든 요청을 처리하기 때문에 Context Switching 비용이 훨씬 줄어든다.

# Spring MVC vs. Spring WebFlux

Spring MVC는 동기/블로킹 모델이며, 멀티 스레드로 동작하는 Tomcat 같은 서블릿 컨테이너를 런타임으로 사용한다. Spring MVC는 One request One thread 모델로 요청이 올 때마다 스레드를 생성하여 전담하게 한다. 이 모델에서는 네트워크나 입출력 등 무거운 작업을 수행하면 스레드가 자원을 점유한 채 대기하게 된다. CPU는 다른 스레드를 실행시키기 위해 Context Switching을 하며, 스레드 수가 많아질 수록 이에 대한 비용이 커지게 된다.

Spring WebFlux는 비동기/논블로킹 모델이며, 보통 Netty 같은 비동기/논블로킹 런타임을 사용한다. Spring WebFlux는 이벤트 드리븐 모델Node.js와 유사하다. 다만 싱글 스레드는 아니며 CPU 코어 수 만큼의 스레드로 병렬처리를 한다. 비동기/논블로킹 모델에서 워커 스레드는 입출력 같이 오랜 시간이 걸리는 작업이 I/O Controller에 의해 처리될 때 이를 기다리지 않고 다른 작업을 수행한다. 다시 말해 스레드를 놀지 않게 하는 것이 핵심이며, 같은 스레드를 사용하기 때문에 Context Switching 비용이 최소화된다.

비동기/논블로킹 모델에서는 다른작업이 종료되었을 때 이를 알려줄 수 있는 방법이 필요한데, 보통 다음과 같은 방법으로 구현한다.

  • Callback를 함께 전달하여 작업이 끝났을 때 호출되도록 한다.
  • 관찰, 구독 가능한 객체로 이벤트를 보내거나 상태 변화를 일으킨다.

관찰, 구독 가능한 형태로 비동기/논블로킹 모델을 구현하는 경우, 서버가 데이터를 생성하는 속도가 클라이언트의 소비 속도보다 빠를 수 있다. 클라이언트는 이를 적절하게 처리하기 위해 배압 이슈를 해결해야 한다.

비동기/논블로킹의 모델은 네트워크 통신이나 데이터 소스도 논블로킹하게 처리해야한다. 그렇지 않으면 스레드 수가 적기 때문에 오히려 동기/블로킹 모델보다 속도가 느려지게 된다. Spring WebFlux의 경우 WebClientSpring Data R2DBC(Reactive Relation Database Connectivity)를 사용하여 네트워크 통신이 데이터 소스도 논블로킹하게 처리한다.

그럼 Spring WebFlux를 간단하게 사용해보자.

# 의존성 추가

Spring MVC의 경우 Servlet 기반의 Tomcat을 사용한다.

dependencies {
    // Spring Boot + Spring MVC
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

따라서 내장 Tomcat에서 애플리케이션이 구동된다.

반면 Spring WebFlux는 Netty를 사용한다.

dependencies {
    // Spring Boot + Spring WebFlux
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

따라서 내장 Netty에서 애플리케이션이 구동된다.

# 사용법

Spring WebFlux는 두 가지 방식으로 API Endpoint를 정의할 수 있다.

  • Spring MVC 어노테이션 방식
  • 함수형 모델

# Spring MVC 어노테이션 방식

Spring MVC는 Servlet Container가 제공하는 HttpServletRequest, HttpServletResponse로 요청과 응답을 처리한다.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class TestController {

    @GetMapping("/test1")
    public void test1(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String name = request.getParameter("name");
        response.getOutputStream().print("response: " + name);
    }

    @PostMapping("/test2")
    public void test2(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // ...
    }
}

물론 HttpServletRequest, HttpServletResponse 대신 Spring MVC가 제공하는 다양한 어노테이션과 Converter로 바인딩할 수 있다.

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class TestController {

    @GetMapping("/test1")
    public String test1(@RequestParam(value="name") String name) {
        return "response:" + name;
    }

    @PostMapping("/test2")
    public String test2(@RequestBody TestRequest request) {
        // ...
    }
}

Spring WebFlux도 Spring MVC와 유사한 방식으로 요청과 응답을 처리할 수 있다. 다만 HttpServletRequest, HttpServletResponse 대신 ServerRequest, ServerResponse를 사용한다. 또한 응답을 Mono 또는 Flux로 래핑해야한다.

import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@Controller
public class TestController {

    @GetMapping("/test1")
    public Mono<ServerResponse> test1(ServerRequest request) {
        // ...
    }

    @PostMapping("/test2")
    public Mono<ServerResponse> test2(ServerRequest request) {
        // ...
    }
}

물론 Spring MVC처럼 바인딩도 사용할 수 있다.

@RestController
public class TestController {

    @GetMapping("/test1")
    public Mono<String> test1(@RequestParam("name") String name) {
        return Mono.just("response: " + name);
    }

    @PostMapping("/test2")
    public Mono<ServerResponse> test2(@RequestBody TestRequest request) {
        // ...
    }
}

요청을 받을 때도 Mono로 래핑할 수 있다. 이 경우 Reactive Stream API가 제공하는 다양한 Operator로 더욱 Reactive하게 코드를 작성할 수 있다.

@RestController
public class TestController {

    // ...

    @PostMapping("/test2")
    public Mono<ServerResponse> test2(@RequestBody Mono<TestRequest> request) {
        return request
            .map(req -> ... )
    }
}

# 함수형 모델

함수형 모델은 두 가지의 함수형 인터페이스를 사용하여 구현한다.

  • HandlerFunction: 요청을 처리하고 응답을 반환한다.
  • RouterFunction: 요청을 라우팅해준다.

요청을 처리하고 응답을 반환하는 핸들러는 다음과 같이 정의할 수 있다.

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import static org.springframework.web.reactive.function.BodyInserters.fromValue;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;


@Component
public class TestHandler {

    public Mono<ServerResponse> test1(ServerRequest request) {
        return ok().body(fromValue("response 1: " + request.queryParam("name").get()));
    }

    public Mono<ServerResponse> test2(ServerRequest request) {
        return ok().body(fromValue("response 2: " + request.queryParam("name").get()));
    }
}

요청을 라우팅해주는 핸들러는 다음과 같이 정의할 수 있다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

@Configuration
public class TestRouter {

    @Autowired TestHandler handler;

    @Bean
    public RouterFunction<ServerResponse> testRoute() {
        return RouterFunctions
            .route(RequestPredicates.GET("/test1"), handler::test1)
            .andRoute(RequestPredicates.GET("/test2"), handler::test2);
    }
}

# WebClient

WebClient는 Spring 5부터 지원하며 비동기/논블로킹 식으로 HTTP 요청을 보내는 HTTP Client다.

# 의존성 설정

WebClient는 Spring WebFlux에 포함되어있다.

dependencies {
    // Spring Boot + Spring WebFlux
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

# 예제

간단한 예제를 살펴보기 위해 두 개의 프로젝트를 생성한다.

  • api
  • client

api 프로젝트에 다음 컨트롤러를 구현한 후 9090 포트로 실행한다.

@RestController
@RequestMapping("/api")
public class ApiController {

    @GetMapping("/get")
    public String get() {
        return "get";
    }
}

client 프로젝트에 다음 컨트롤러를 구현한 후 8080 포트로 실행한다.

import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/client")
public class ClientController {

    @GetMapping
    @RequestMapping("/get")
    public Mono<String> get() {
        WebClient webClient = WebClient.create();
        return webClient.get()
                .uri("http://localhost:9090/api/get")
                .retrieve()
                .bodyToMono(String.class);
    }
}

이제 웹 브라우저에서 http://localhost:8080/client/get 으로 접속하면 WebClient가 작동하는 것을 확인할 수 있다.

# WebClient 생성

WebClient는 두 가지 방법으로 생성할 수 있다. 우선 WebClient.create() 메소드를 사용할 수 있다.

WebClient webClient = WebClient
    .create("http://localhost:9090");

WebClient.build() 메소드를 사용할 수도 있다.

WebClient webClient = WebClient.builder()
    .baseUrl("http://localhost:9090")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .build();

보통 다음과 같이 컨테이너에 빈으로 등록하여 사용한다.

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient webClient = WebClient.builder()
            .baseUrl("http://localhost:9090")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }

}

# GET 요청 보내기

Mono<Person> result = webClient.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono(Person.class);
Flux<Person> result = webClient.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToFlux(Person.class);
Mono<ResponseEntity<Person>> result = webClient.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .toEntity(Person.class);

# POST 요청 보내기

Person person = Person.builder().build();

Mono<Person> result = webClient.post()
    .uri("/persons")
    .body(Mono.just(person), Person.class)
    .retrieve()
    .bodyToMono(Person.class);

# PATCH 요청 보내기

Person person = Person.builder().build();

Mono<Person> result = webClient.patch()
    .uri("/persons/" + 1)
    .body(Mono.just(person), Person.class)
    .retrieve()
    .bodyToMono(Person.class);

# DELETE 요청 보내기

Mono<Void> result = webClient.delete()
    .uri("/persons/" +id)
    .retrieve()
    .bodyToMono(Void.class);

# @WebFluxTest, TestWebClient

@WebFluxTest어노테이션, TestWebClient클래스를 사용하면 WebFlux 기반 API Endpoint를 테스트할 수 있다. 다음과 같은 컨트롤러가 있다고 하자.

@RestController
@RequestMapping("/client")
public class ClientController {

    @GetMapping
    @RequestMapping("/get")
    public Mono<String> get() {
        return Mono.just("get");
    }
}

@WebFluxTest어노테이션과 TestWebClient클래스는 다음과 같이 사용한다.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.web.reactive.server.WebTestClient;

@WebFluxTest
class ApplicationTests {

    @Autowired
    private WebTestClient webClient;

    @Test
    void test() {
        webClient.get()
                .uri("/client/get")
                .exchange()
                .expectStatus().isOk();
    }
}