본문 바로가기
Java/Java

Spring MVC와 Spring WebFlux로 알아보는 Blocking I/O vs Non-blocking I/O

by oneny 2023. 9. 5.
Spring WebFlux에 대해 깊이있게 공부하기 보다는 Spring MVC와 Spring WebFlux를 통해 Blocking 방식과 Non-blocking 방식에서 스레드가 어떻게 동작하는지에 대해서 가볍게 알아보려고 합니다.

 

관련 블로그

 

blocking I/O vs non-blocking I/O (feat. socket I/O)

I/O I/O는 input/output의 약자로, 데이터의 입출력을 의미한다. 이러한 I/O의 종류에는 일반적으로 우리가 알고 있는 file I/O, 프로세스 간의 통신을 할 때 사용되는 pipe I/O이 있다. 그리고 일반적인 디

oneny.tistory.com

위에서 Blocking I/O와 Non-blocking I/O에 대해서 공부했던 글이다.

 

Spring MVC와 Spring Webflux

Spring MVC와 WebFlux의 공통점은 @Controller, Reactive 클라이언트이며, 둘 다 Tomcat, Jetty, Undertow와 같은 서버에서 실행할 수 있다. Spring MVC에서는 명령형 논리, JDBC, JPA를 가질 수 있으며, Spring WebFlux에서는 기능적 엔드 포인트, 이벤트 루프, 동시성 모델을 가지며 Netty 서버에서 실행할 수 있다는 장점이 있다.

 

Spring Webflux

Spring Webflux는 Spring 5에서 새롭게 추가된 모듈이다. Webflux는 클라이언트, 서버에서 reactive 스타일의 애플리케이션 개발을 도와주는 모듈이며, reactive-stack web framework이며 non-blocking의 reactive stream을 지원한다.

 

등장 배경

WebFlux가 생긴 이유는 다음과 같다.

  1. 적은 양의 스레드와 최소한의 하드웨어 자원으로 동시성을 핸들링하기 위해 만들어졌다.
  2. 함수형 프로그래밍 때문이다. Java5에서 RestController나 Unit Test가 만들어지고, Java8에서 함수형 API를 위한 람다식이 추가되었는데 이는 non-blocking 애플리케이션 API의 토대가 되었다.

 

Spring WebFlux가 등장하게 된 배경에 대해 예시를 통해 알아보자.

위 그림처럼 A가 B에게 몇 시인지를 요청한다고 가정해보자. 이 때, B도 시계가 없어 시계를 가지고 있는 C에게 요청을 한다. 따라서 b의 요청을 받은 거실에 있는 C는 안방에 있는 시계를 보러 걸어가 확인한 후, B에게 응답을 한다. 응답은 받은 B는 응답받은 시간을 다시 A에게 응답하여 A의 요청에 대한 작업을 모두 처리했고, A가 응답을 받기까지 5초 + α 시간이 걸렸다.

이제 A를 Client, B를 Server, C를 DB라고 생각해보자. 위 같이 하나의 코어 기준 Synchronous, Blocking 방식으로 진행하는 경우에는 다음과 같은 세 가지 문제가 발생한다.

  1. 만약 3명의 클라이언트가 서버에 요청을 하는 경우에는 첫 번째 사람은 5초 + α 시간이 걸릴 수 있지만, 그 뒤 사람들은 10초 이상, 15초 이상이 걸리게 된다.
  2. 클라이언트와 서버가 DB에서 처리하는 약 5초라는 시간 동안 아무것도 하지 못하는 상태가 된다는 것이다.
  3. 클라이언트가 몇 시인지를 알고 싶을 때마다 계속 서버에 요청을 해야 한다는 문제가 있다.

 

먼저, 1번 문제를 해결하기 위한 방법에 대해서 알아보자. 아까 세 번째 클라이언트는 지연 시간이 15초 + α시간이 걸린다고 했는데 이를 해결하기 위해서는 클라이언트의 요청이 들어올 때마다 스레드를 생성해 여러 스레드를 동시에 시작하도록 만들면 된다. 하지만 프로그래밍상 하나의 코어는 한 번에 하나의 명령어만 처리할 수 있기 때문에 동시적으로 실행하는 것은 불가능한 일이며 동시적으로 실행하는 것처럼 만들기 위해서 시분할(time-slicing)을 사용한다. 그러면 하나의 코어이더라도 두 개 이상의 스레드가 빠르게 교차되면서 실행되기 떄문에 동시라고 느끼는 것이다. 따라서 코어의 개수보다 많은 스레드가 들어오더라도 이 시분할 방법을 통해 동시성(인 것처럼)을 보장할 수 있는 것이다.

 

하지만 이러한 방법에도 문제가 있다. 위 그림처럼 코어 개수 이상의 스레드 요청이 들어오게 되면 작업하고 있는 스레드와 대기하고 있는 스레드가 교차되면서 컨텍스트 스위칭(Context Switching)의 시간이 발생한다. 따라서 이를 해결하기 위해서 코어 개수만큼 스레드를 만들어 컨텍스트 스위칭이 발생하지 않도록 하고, 위처럼 동시성을 보장하기 위해 비동기적인(Asynchronous) 작업을 지원하는 기술이 등장했다.

 

똑같은 상황에서 DB는 이전처럼 시간을 확인하러 가지만 이전과는 다르게 요청한 결과가 준비되지 않았음에도 아직 결과가 없다는 메시지와 함께 바로 응답을 한다. 그리고 서버도 마찬가지로 아직 몇시인지 결과를 받지 못했다고 클라이언트에게 알려준다. 그러면 서버가 클라이언트에 대한 요청을 모두 처리했으며 다른 클라이언트 요청을 받을 준비가 됐다.

그래도 서버의 결과를 클라이언트가 알지 못하기 때문에 서버는 DB의 결과를 응답받으면 클라이언트에게 알려줘야 한다. 따라서 미처 결과를 처리하지 못한 클라이언트의 요청 즉, 이벤트를 기억하기 위해 이벤트 루프라는 기억공간에 저장하며 다른 클라이언트의 요청을 계속해서 받는다.

 

이렇게 계속해서 요청을 받다 잠깐 요청이 안들어오는 타이밍을 활용하여 서버는 그 잠깐을 쉬지 않고, 이벤트 루프를 확인하여 이벤트가 종료되어 결과를 응답해줘야 하는 클라이언트들의 요청을 다시 응답해준다. 이러한 방식을 비동기적인(Asynchronous) 방식이라 하고, 서버가 DB의 처리 결과가 끝나기를 기다리지 않고, DB가 아직 데이터가 없다는 메시지를 받아 다시 그대로 진행하는 방식을 Non-blocking 방식이라고 한다. 이러한 방식 덕분에 위 문제 2번 결과를 기다리기까지 아무것도 하지 못하는 상태가 되는 것을 해결할 수 있다.

이전에는 스레드를 이용하여 시분할(time-slicing)을 통해 비동기적으로 처리를 할 수 있었다면 이제는 이벤트 루프 덕분에 비동기적으로 처리할 수 있게 되어 스레드는 코어 개수만큼만 설정해 컨텍스트 스위칭 문제를 해결할 수 있게 되었다.

 

HTTP 프로토콜은 Stateless와 Connectionless의 특성으로 서버에서 클라이언트의 이전 상태를 유지하지 않고, 클라이언트가 응답을 받으면 TCP/IP 연결을 끊어 연결을 유지하지 않는다. 이러한 방식은 Non-blocking 방식을 하기에는 힘들기 때문에 응답 지속적인 응답을 할 수 있게 응답을 유지시켜줘야 한다. 여기서 유지를 시킨다는 것은 이전 Socket 게시글에서도 알아봤듯이 Stream 즉, 통신 채널을 만들어놓는 것이고, 끊기지 않고 유지시키는 것이다. 하지만 요청은 유지시킬 필요가 없기 때문에 요청은 끊어버리고 응답만 유지시키는 방식으로 서버 쪽에서 주도적으로 보낼 수 있는 이벤트라 하여 SSE(Server Sent Event) 프로토콜이라고 한다.  이 방식을 통해서 3번 클라이언트에서 몇 시인지 계속해서 요청을 보내지 않고도 서버에서 응답을 유지해 계속해서 몇 시인지를 알려줄 수 있다.

마지막으로 Stream을 다른 말로하면 Flux라고 하고, 이 기술을 Spring에 적용시킨 것이 WebFlux라고 할 수 있다.

 

WebFlux의 특징

  • 장점: 고성능, Spring과 완벽한 통합, Netty 지원, 비동기 non-blocking 메세지 처리
  • 단점: 오류처리가 다소 복잡하다.

즉, WebFlux는 Asynchronous Non-blocking I/O 방식을 활용하여 성능을 끌어올릴 수 있는 장점이 있지만, Non-blocking 기반으로 코드를 작성하면 오류를 처리하기가 다소 복잡하다는 단점이 있다.

 

Mono와 Flux

Spring WebFlux에서 사용하는 Reactive Library가 Reactor 이고, Reactor가 Reactive Streams의 구현체이다. Flux와 Mono는 Reactor 객체이며, 차이점은 발행하는 데이터 개수이다.

  • Flux: 0 ~ N 개의 데이터 전달
  • Mono: 0 ~ 1 개의 데이터 전달

보통 여러 스트림을 하나의 결과로 모아둘 때 Mono를, 각각의 Mono를 합쳐서 여러 개의 값을 처리할 때 Flux를 사용한다. Flux도 0 ~ 1개의 데이터 전달이 가능한데 데이터 설계를 할 때 결과가 없거나 하나의 결과값만 받는 것이 명백한 경우에 List나 배열을 사용하지 않는 것처럼, Multi Result가 아닌 하나의 결과셋만 받게 될 경우에는 불필요하게 Flux를 사용하지 않고 Mono를 사용한다. 

 

Spring MVC와 Spring WebFlux 스레드 상태 확인하기

이제부터 동일하게 응답하는 Spring MVC와 Spring WebFlux 프로젝트를 만들어 JMeter로 동시에 요청하도록 만들어 VisualVM에서 ThreadDump로 스레드의 상태들을 확인할 예정이다.

 

Spring MVC

 

Repository

public interface StudentRepository extends JpaRepository<Student, Integer> {
}

Spring Data JPA를 사용하였다.

 

Service

public class StudentService {
  public List<Student> findAll() {
    return repository.findAll();
  }
}

 

Controller

@RestController
@RequestMapping("/api/rest/students")
@RequiredArgsConstructor
public class StudentController {

  @GetMapping
  List<Student> findAll() {
    return service.findAll();
  }
}

정말 간단하게 학생들을 모두 조회하는  API를 하나 만들었다.

 

Spring WebFlux

 

Repository

public interface StudentRepository extends ReactiveCrudRepository<Student, Integer> {
}

 

Service

@Service
@RequiredArgsConstructor
@Slf4j
public class StudentService {

  private final StudentRepository repository;

  public Flux<Student> findAll() {
    return repository.findAll();
  }
}

 

Controller

@RestController
@RequestMapping("/api/reactive/students")
@RequiredArgsConstructor
public class StudentController {

  @GetMapping
  Flux<Student> findAll() {
    return service.findAll();
  }
}

Spring WebFlux도 Spring MVC와 마찬가지로 학생들을 모두 조회하는 API를 작성했다.

 

 

JMeter 설정

해당 모든 학생들을 조회해오는 요청을 동시에 10000번 요청하도록 JMeter를 설정했다.

 

Spring MVC의 스레드 상태 확인

JMeter 시작 버튼을 누르니 기존에 있던 10개의 스레드에서 190개가 늘어나 200개가 되는 것을 확인할 수 있었다.

 

또한 Thread Dump를 떠서 RUNNABLE인 상태가 몇 개인지 확인했는데 http-nio-8080-exec-(번호)의 RUNNABLE 상태인 것이 10개인 것을 확인했고, 나머지는 TIMED_WAITING 상태였다. 

 

그리고 다시 빠르게 Thread Dump를 떠서 확인해보니 RUNNABLE이였던 164 스레드가 TIMED_WAITING인 것을 확인할 수 있었다. 이렇게 Synchronous + Blocking 방식이지만 하나의 요청에 대해 하나의 스레드를 할당하여 처리하고, 코어 대비 스레드 개수가 많아도 타임 슬라이싱을 통해 스레드가 빠르게 교차하면서 동시성을 나타내는 것을 확인할 수 있었다.

 

Spring WebFlux 결과

 

고정된 스레드 수만으로 모든 요청을 처리하기 때문에 스레드가 10개까지만 늘어난 것을 확인할 수 있었다. 서버는 스레드 한 개로 운영하며, 디폴트로 CPU 코어 개수의 스레드를 가진 워커풀을 생성하여 해당 워커 풀 내의 스레드로 모든 요청을 처리한다.

 

생성된 스레드 10개가 모두 RUNNABLE인 상태였고, 바로 다시 Thread Dump를 떠본 결과를 확인해보자.

 

느린 손가락으로 2초 뒤에 Thread Dump를 떠봤는데도 reactor-http-nio-2가 계속해서 RUNNABLE인 것을 확인할 수 있었다. 이를 통해 Spring MVC처럼 스레드를 생성하여 타임 슬라이싱을 통해 여러 스레드들이 교차하면서 동시성을 나타낸 것과 달리 Spring WebFlux는 Asynchronous + Non-blocking 방식이기 때문에 코어 개수에 맞춰 컨텍스트 스위칭이 일어나지 않으면서 비동기적인 작업을 통해 동시성을 나타내도록 하는 것을 확인할 수 있었다.

 

 

출처

[Spring] Webflux의 개념 / Spring MVC와 간단비교

[Spring] Webflux란 무엇인가? - 개념(특징), MVC와 비교, 사용 이유

Springboot-WebFlux 2강 - Reactive Programming 배경

Springboot-WebFlux 3강 - WebFlux 탄생