프로젝트/기술적 선택

[기술적 선택] Webflux의 적용한 이유와 동작 흐름

배발자 2023. 7. 22.
반응형

 

필자는 알림 서버를 활용하기 위해 WebFlux라는 웹 프레임워크를 활용하였다. 

우리가 진행한 프로젝트는 개발자 커뮤니티 서비스를 만드는 것이였고, 내부적으로 여러 서비스를 제공한다. 해당 프로젝트는 옛 싸이월드 감성으로 제작된 프로젝트였기 때문에 일촌 신청, 방명록, 댓글, 일촌 수락 등 사용자의 커뮤니티를 활성화하기 위한 서비스가 주목적이다. 따라서 다른 사용자들이 요청한, 또는 작성한 컨텐츠들과 관련된 사람들에게 알림 서비스를 제공한다. 즉, 여러 클라이언트들이 동시다발적으로 컨텐츠 요청이 들어올 경우 이를 처리하기 위해선 동기적인 방식이 아닌 새로운 방식을 고민해봐야했다. 

 

고민 끝에 blocking 방식보다는 적은 리소스로 동시성을 다루는 non blocking 방식을 생각하게 되었고, 일반적으로 사용하는 blocking 방식의 Spring MVC보다 event driven, non blocking 한 처리가 가능한 WebFlux를 사용하는 것을 선택했다. 

 

 

용어 정리 

 

동기(Sync)

동기는 말 그대로 동시에 일어날 수 있다는 뜻인데, 호출과 응답이 동시에 이루어지는 것을 말한다. 

다시 말해, 함수를 호출한 곳에서 바로 응답을 받는다. 

 

비동기(Async)

비동기는 호출과 응답이 동시에 이루어지지 않는 것을 의미한다. 

호출 시점과 처리결과에 대한 응답시점이 같지 않아서 함수를 호출했을 때 그에 대한 처리결과를 추후에 처리가 완료된 불특정 시점에 전달받는다. 

 

블로킹(Blocking)

함수를 call 했을 대 응답을 받기 위해 멈춰있는 상태를 의미한다. 

기본적으로 코딩테스트를 진행할 때 메인 메소드 내부에서 method() 함수를 호출했을 때 해당 함수가 수행된다. 이때 해당 함수의 로직이 수행되고 함수의 return이 있고나서 함수가 종료된다. 이후 메인 메소드에서 해당 함수를 호출한 다음 줄의 코드를 실행할 수 있다. 

 

논블로킹(Non-Blocking)

블로킹 방식과 다르게 메인 메소드 내부에서 method() 함수를 호출하더라도 그 결과를 리턴받지 않아도 바로 다음줄을 실행할 수 있는 상태를 의미한다. 즉, method() 함수에서 리턴되지 않고 아직 수행 중이더라도 메인 메소드는 다음 줄을 수행한다. 

 

 

 

Spring MVC vs Spring WebFlux 

 

 

스프링 공식 문서 : https://docs.spring.io/spring-framework/reference/

 

 

MVC는 벽에다가 공 던져서 다시 받기
Webflux는 공을 기차에 실어서 기차가 한바퀴 돈 후 공을 다시 받기

 

 

 

1. Spring MVC

 

​Spring MVC는 Servlet 기반으로 만들어졌고, sync + blocking 방식으로 동작하고 결국 하나의 처리를 할 때 Response를 기다리며 thread를 지연시키는 부분이 있다.

 

이해하기 어려울 수 있는데 기본적으로 사용자 요청이 Spring 서버에 들어오게 된다면, ThreadPool에서 존재하는 스레드를 하나 할당하고 해당 스레드는 그 요청을 처리해야한다. 만약 사용자 요청이 디스크에 오래 걸리는 작업을 요청할 경우 해당 스레드는 디스크 쓰기 작업이 완료될 때까지 기다리는 상황이 생기는데 이를 Blocking이라고 하는 것이다. 

 

따라서 Spring MVC 같은 경우 요청이 들어오면 그 요청을 Queue에 쌓고 순서에 따라서 Thread를 하나 점유해 요청을 처리한다. 동시 다발적으로 스레드 수를 초과하는 요청이 발생한다면 계속해서 요청이 큐에 대기하게 되는 Thread Poll Hell 현상이 발생할 수 있다. 이를 해결하기 위해 시스템의 트래픽을 측정해서 thread pool size를 잘 조정해야한다. 하지만 사이즈를 조정을 하더라도 사용자의 요청을 대량으로 받아내는데는 한계가 있을 것이다. 이를 해결하는 것이 Webflux인 것이다. 

 

2. Webflux

 

Webflux는 요청을 처리하는 방식이 Event-Driven 방식이고 async + nonblocking 방식이다. 

간단하게 설명하자면, 이벤트 루프가 돌아서 요청이 발생할 경우 맞는 핸들러에게 처리를 위임하고 처리가 완료되면 callback 메소드 등을 통해 응답을 반환한다. 

 

요청이 처리될 때까지 기다리지 않기때문에 사용자의 요청을 대량으로 받아낼 수 있다는 장점이 있다. 

즉, 서버 프로그램이 효율적으로 동작해서, cpu, thread, memory에 자원을 낭비하지 않고 효율적으로 동작하는 고성능 웹 애플리케이션을 개발하는 목적으로 한다. 서비스간 호출이 많은 MSA에 적합한 것이다. 

 

Spring MVC WebFlux
동기(Synchronous)적인 방식 비동기(Asynchronous)적인 방식
블로킹(Blocking) 방식으로 구현 논 블로킹(Non-Blocking) 방식으로 구현
명령형 프로그래밍 반응형 프로그래밍
JDBC JPA 네트워킹 지원 반응형 라이브러리(Reactor, RxJava) 지원 

 

Netty 

 

Netty는 프로토콜 서버 및 클라이언트와 같은 네트워크 응용 프로그램을 빠르고 쉽게 개발할 수 있는 NIO (Non-Blocking Input / Output) 클라이언트 서버 프레임 워크이다.  TCP 및 UDP 소켓 서버와 같은 네트워크 프로그래밍을 크게 단순화하고 간소화한다. 

 

Spring Boot는 기존의 서블릿 기반의 Tomcat을 기반으로 동작한다. 반면 Spring Boot WebFlux는 여러 가지를 고를 수 있지만 기본적으로 Netty를 사용한다. Tomcat은 요청 당 하나의 스레드를 동작하는 반면, Netty는 1개의 이벤트를 받는 스레드와 다수의 Worker 스레드로 동작하게 된다. 조금 더 깊게 살펴보자. 

 

Netty는 채널에서 발생하는 이벤트를 EventLoop가 처리하는 구조로 동작하게 된다. 

이벤트 루프는 이벤트를 실행하기 위한 무한루프 스레드를 뜻한다. 

 

그림 

 

객체에서 발생한 이벤트는 이벤트 큐에 push되고 이벤트 루프는 이벤트 큐에 입력된 이벤트가 있을 때 해당 이벤트를 꺼내서 실행하게 된다. 이벤트 루프는 스레드의 종류에 따라서 싱글 스레드, 멀티 스레드로 나누어지게 된다. 

 

싱글 스레드

싱글 스레드는 이벤트 루프에서 하나의 스레드로 동작하게 된다. 단일 스레드이기 때문에 예측 가능한 동작을 보장하고, 이벤트 루프의 구현이 단순하다는 장점이 있다. 하지만 하나의 처리 시간이 긴 작업이 들어오면 전체 작업 지연 시간이 늘어난다는 단점이 있다. 그러므로 멀티 코어 CPU 환경에서 CPU 자원을 효율적으로 사용할 수 없다. 

 

멀티 스레드

멀티 스레드는 이벤트 루프에서 이벤트를 처리하는 스레드를 여러개를 두는 모델이다. 싱글 스레드 모델보다는 구현이 복잡해지지만 스레드들이 이벤트 메서드를 병렬로 수행하기 때문에 CPU 자원을 효율적으로 사용할 수 있다. 하지만 여러 이벤트 스레드가 하나의 이벤트 큐에 접근하기 때문에 동시성 이슈 문제가 발생할 수 있다. 또한, 병렬로 처리되기때문에 이벤트의 발생순서와 실행 순서가 일치하지 않는다. 

 

이러한 문제점을 해결하기 위해 아래와 같은 방법으로 해결한다. 

 

 - Netty의 이벤트는 Channel에서 발생

-  각각의 이벤트 루프 객체는 개인의 이벤트 큐를 가짐 

- Netty Channel은 하나의 이벤트 루프에 등록 

- 하나의 이벤트 루프 스레드에는 여러 채널이 등록 

 

루프들이 이벤트 큐를 공유하여 이벤트 발생순서와 실행 순서가 일치하지 않는다.

Netty는 이벤트 큐를 이벤트 루프 스레드의 내부에 둠으로써 실행 순서의 불일치 원인을 제거하게 된다. 

 

 

Stream

 

Http 프로토콜 방식은 stateless이기 때문에 클라이언트가 요청을 보내고 server가 응답을 한 번 보내면 연결을 끊어버린다. 그렇게 된다면, 이벤트 루프에서 기억하고 있었던 응답을 다시 보낼 수가 없다. 

이러한 문제점을 해결하기 위해 응답을 Stream으로 만들어서 끊기지 않게 계속 유지하는 방법을 SSE 프로토콜 방식이라고 한다. 

 

 

 

SSE 웹 기술을 활용한 이유 (알림 기능)

HTTP 프로토콜은 기본적으로 클라이언트가 요청을 보내고 서버에서 응답을 보내주면 연결을 끊어버린다. 채팅과 같은 서비스에서 계속 연결을 유지하고 응답 데이터가 지속적으로 이루어져야한

baebalja.tistory.com

 

필자가 Webflux를 활용한 가장 큰 이유 중 하나이기도 하다. 물론 여러 알림 요청을 처리하기 위해 비동기적인 방식을 수행하는 용도이기도 하지만 SSE 프로토콜 방식을 통해 알림 서비스를 구현한 것이다. 

물론, 웹 소켓을 활용하여 알림 서비스를 구현할 수는 있겠지만 웹 소켓처럼 요청과 응답을 양방향 통신을 하는 것은 리소스적으로 낭비라는 것이다. 

 

 


 

이벤트 루프와 Stream 같은 SSE 프로토콜 방식으로 반응형 프로그래밍을 할 수 있다. 

Webflux는 구독과 출판의 개념을 가지고 있는데 구독은 Response가 유지되고 있다. Publish는 유지되고 있는 선으로 계속적으로 데이터를 응답해준다. 

 

다른 사람이 어떤 이벤트를 요청해서 데이터의 변경이 일어나면, 서버가 그 데이터를 바라보고 있는 상대들한테 즉각적으로 Push 해줄 수 있다. 일반적으로 RDBMS는 지원하지 않고 리액티브를 지원하는 MongoDB, Redis를 사용해야하지만 최근에는 R2DBC 라이브러리로 MySQL처럼 RDBMS에서도 비동기 방식으로 연결이 가능하다고 한다. 

 

 

Webflux는 Spirng 5 이후 추가된 모듈이다. 

WebMVC를 토대로 프로젝트를 진행할 때 관련된 자료들이 워낙 많아서 서비스를 구현하는데 크게 어려운 점이 없었다. 하지만 Webflux를 활용해 알림 서비스를 구현하는 것은 자료가 부족해서 꽤나 힘들었다. 

 

 


 

Mono & Flux 

 

Spring Webflux에서 사용하는 reactive library가 Reactor이고 Reactor가 Reactive Streams의 구현체이다. 

Reactor의 주요 객체로 Mono와 Flux가 있다. 

 

 

mono : 한 번 응답하고 끊나는 방식으로 0~1개의 데이터 요청에 대해 응답할 때 사용

flux : 지속적으로 응답하는 방식으로, 1개 이상의 데이터 요청에 대해 응답할 때 사용 

 

 

Mono

 

 

Reactive Streams의 Publisher 인터페이스를 구현하는 구현체인데, Flux와의 차이점은, Flux는 0~N개의 데이터를 처리하지만, Mono는 0~1개의 데이터를 처리한다. 데이터의 전달처리가 완료되면 onComplete, 데이터를 전달하는 과정에서 오류가 발생하면 onError로 종료된다. 하나의 응답 결과만 리턴하면 되기 때문에 별도로 값은 필요없고, 완료 개념만 있으면 비동기 처리도 표현할 수 있다. 

 

 

Flux

 

 

Flux는 Reactive Streams에서 정의한 Publisher의 구현체로서, 0~N개의 데이털르 발행(전달, 방출)할 수 있다. 하나의 데이터를 전달할 때마다 onNext 이벤트를 발생한다. Flux내의 모든 데이터의 전달 처리가 완료되면 onComplete 이벤트가 발생하며, 데이털르 전달하는 과정에서 오류가 발생하면 onError 이벤트가 발생한다. 즉, onComplete나 onError가 되기 전까지는 무한 생성가능한 Stream으로 생각해야한다. 

 

 

 

참고자료

 

반응형

댓글