오늘의 나보다 성장한 내일의 나를 위해…













:pushpin: Spring WebFlux


우리가 보통 사용하던 Spring MVC + RDBMS 패턴Blocking IO 방식이다.

Blocking IO 방식이라는 것은 요청을 처리하기 전까지는 다른 작업을 수행할 수 없는 상태라는 것을 말한다.

동시에 여러 요청처리하기 위해서는 Thread 수를 늘려서 하는 방법이 존재하기는 하지만 오버헤드가 발생한다.

이를 개선하기 위해서 나온 기술이 Non-Blocking IO 방식인 Spring WebFlux이다.

Spring WebFlux동시에 처리되어야 할 많은 요청에 대해 효율적으로 처리해 줄 수 있다.


스레드 풀을 이용한 동기식 호출 방식은 코드가 간단하고 순차적으로 동작하기 때문에 개발자가 코드를 직관적이고 빠르게 작성할 수 있다.

하지만 이렇게 작성한 코드로 만든 서버도 빠르게 동작하고 많은 요청을 처리할 수 있을까?

동기식 호출 방식에서는 상대편의 응답이 올 때까지 스레드는 기다려야(blocking)한다.

응답이 빨리 오면 그 기다림은 길지 않겠지만 만약 응답이 늦게 오면 서버가 요청에 대한 응답을 기다리는 데 스레드를 모두 소진해서 추가 요청을 처리할 수 없는 상태가 될 수 있다.

특히 MSA에서는 타임아웃이 발생할 정도의 지연이 발생하면 순식간에 다른 모듈로 전파되어 전체 시스템이 마비되는 등의 악영향을 끼칠 수 있다.


여기서 의문점이 생긴다.

쓰레드가 서버로 요청을 하고 나서 꼭 응답을 기다리면서 아무 것도 하지 않고 대기해야 할까?

쓰레드가 응답을 기다리지 않고 다른 일을 처리하다가 응답이 왔을 때 해당 일을 처리한다면 응답만 기다리면서 불필요하게 리소스를 점유하는 일은 없을 것이다.

이러한 요구 사항에서 나온 것이 이벤트 루프를 이용한 비동기 프로그래밍이다.


이벤트 루프를 활용하면 요청을 보내고 응답이 올 때까지 무작정 기다리는 대신 자신에게 할당된 다른 여러 소켓의 요청을 순차적으로 빠르게 처리한다.

이제 우리의 서버와 클라이언트의 스레드는 더이상 blocking되지 않는다.

Spring 생태계에서도 버전 5부터 도입된 WebFlux를 통해 비동기 프로그래밍을 본격적으로 도입하고 있다.


물론 단점도 있다.

순차적으로 처리되는 방식이 아니라 디버깅이 힘들고 개발이 어렵다.

즉 SI에서 사용하기는 좀 힘든 부분이 있다.


:pushpin: Example


아래 예제는 Spring.io에 있는 간단한 Reactive RESTful Web Service이다.

원문 링크


원문을 하나하나 해석해 보자.


목적


이제부터 우리는 Spring WebFlux를 이용하여 RESTful web service와 service의 WebClient consumer를 만들게 될 것이다.

우리는 아래로 요청했을 때의 System.out의 출력 값을 확인할 수 있다.

http://localhost:8080/hello


시작 전 필요한 것들

  • 15분 정도의 시간

  • 좋은 텍스트 에디터 아니면 IDE

  • JDK 1.8 이상

  • Gradle 4+ 아니면 Maven 3.2+

  • 코드를 IDE로 바로 가져올 수도 있음


pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
  </dependency>

  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>io.projectreactor</groupId>
    <artifactId>reactor-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>


이중 pom.xml 을 열어보면 이렇게 spring-boot-starter-webflux dependency를 가지고 있는것을 확인할 수 있다.

이번엔 hello package 내의 클래스를 하나씩 보면서 어떤 방식으로 동작하는지 알아보자.


Handler

package hello;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;

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

@Component
public class GreetingHandler {

    public Mono<ServerResponse> hello(ServerRequest request) {

        return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
                .body(BodyInserters.fromValue("Hello, Spring!"));
    }
}


Spring Reactive 접근 방식에서는 Handler를 사용하여 요청을 처리하고 응답을 생성한다. 이 예제에서는 요청에 대해 “Hello, Spring!”을 반환하는 역할을 해주고 있다. 여기서 Mono라는 객체가 나오는데 이것은 Reactor에서 결과값을 처리하기 위한 객체라고 이해하면 된다. 주로 0개~1개의 결과값은 Mono에 담고 Flux 객체는 Mono와 유사한데 0개~n개의 결과값을 처리하는 객체이다.


Router

package hello;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
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 GreetingRouter {
    @Bean
    public RouterFunction<ServerResponse> route(GreetingHandler greetingHandler) {
        return RouterFunctions
                .route(RequestPredicates.GET("/hello").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), greetingHandler::hello);
    }
}


Spring MVC의 Controller의 역할 중 RequestMapping의 역할을 하는 녀석이 Router라고 생각하면 이해가 빠를것 같다. 예제에서는 /hello 라는 요청을 greetingHandler::hello 에 매핑을 시켜주고 있다.


WebClient

package hello;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class GreetingWebClient {
    private WebClient client = WebClient.create("http://localhost:8080");
    private Mono<ClientResponse> result = client.get()
            .uri("/hello")
            .accept(MediaType.TEXT_PLAIN)
            .exchange();
    public String getResult() {
        return ">> result = " + result.flatMap(res -> res.bodyToMono(String.class)).block();
    }
}


WebClient는 외부와 통신을 하는 창구 역할을 한다고 보면 된다.

기존에는 RestTemplate을 이용해서 HTTP 통신을 했지만 이것은 Blocking IO 방식이다. 그래서 Non-Blocking IO 방식비동기 방식을 지원하기 위한 WebClient를 만들었고 WebFlux에서는 이것을 써야 한다.

가장 간단한 예제라 create()를 통해서 WebClient를 생성했지만 다른 옵션들을 더 추가하려면 build()를 써야 한다.


Aplication.java

package hello;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        GreetingWebClient gwc = new GreetingWebClient();
        System.out.println(gwc.getResult());
    }
}



서버는 tomcat 이 아닌 embedded Netty를 사용한다. Application.java 에서 WebClient에서 result를 가지고 오라고 되어 있어서 구동할때도 result = Hello, Spring! 을 출력해준다. 또한 http://localhost:8080/hello 를 입력하면 result 인 Hello, Spring! 가 출력되는 것을 볼수 있다.


YoungKyonYou

Integration of Knowledge