본문 바로가기

프로젝트/웹 SNS 타임캡슐

WebFlux vs SSEemitter 분석

목차

    먼저 결론

    둘 다 비동기, 그러나 IO 작업에서 효율의 차이 발생 (논블로킹과 블로킹)

    • SSEEmitter: 비동기 서블릿을 사용하지만, 내부적으로는 Java의 전통적인 블로킹 I/O API를 사용할 수 있다. 따라서 I/O 작업이 길어지면 해당 작업을 수행하는 쓰레드는 블로킹될 수 있다.
    • WebFlux: 이벤트 루프와 논블로킹 I/O를 사용합니다. 이로 인해 적은 수의 쓰레드로도 높은 동시성을 처리할 수 있고, 복잡한 로직이나 큰 규모의 애플리케이션에서는 특히 더 효율적이다.

    따라서 복잡한 로직이나 높은 동시성이 필요한 경우, WebFlux가 더 효율적으로 동작할 수 있다.

    SeeEmitter는 비동기지만 Blocking IO이다.

    WebFlux와 SseEmitter 모두 비동기 처리를 지원하지만, 둘의 비동기 처리 방식과 효율성은 다르다.

    WebFlux는 리액티브 프로그래밍 패러다임을 기반으로 하며, 논블로킹 IO를 통해 매우 높은 수준의 쓰레드 효율성을 달성할 수 있다.

    **SseEmitter**는 비동기 서블릿을 사용하여 비동기 처리를 수행한다. 이는 리액티브 프로그래밍보다는 전통적인 비동기 처리 방식에 가깝다.

    1. IO Blocking과 관계: **SseEmitter**의 경우, 내부적으로는 비동기 서블릿을 사용하여 비동기 처리를 하지만, 이는 여전히 블로킹 IO에 기반한다. 이는 쓰레드를 완전히 효율적으로 사용할 수 없음을 의미한다.
      물론, 비동기로 처리되므로 여전히 효율은 높아지지만, WebFlux처럼 논블로킹 IO를 사용한 리액티브 프로그래밍에 비하면 상대적으로 비효율적이다.
    2. 리소스 효율성: WebFlux는 논블로킹 이벤트 루프를 사용하여 쓰레드를 극도로 효율적으로 관리한다.
      쓰레드가 IO 작업을 대기할 필요가 없으므로, 동시에 많은 연결을 처리할 수 있고, 트래픽이 높은 환경에서는 WebFlux가 더 효율적일 수 있다. 이 부분을 고려했다.

    결론적으로, WebFlux는 쓰레드 리소스 처리 측면에서 더 효율적인 방식을 제공한다.

    이는 논블로킹 IO와 리액티브 프로그래밍의 이유가 크다.

    SSE는 왜 Blocking IO로 동작하나?

    전통적인 서블릿의 동작

    • 클라이언트 요청이 들어오면 쓰레드 풀에서 하나의 쓰레드가 할당되어 요청을 처리한다.
    • 이 쓰레드는 데이터베이스 쿼리, 파일 읽기 등 I/O 작업이 완료될 때까지 블로킹된다.
    • 이렇게 설계된 이유는, 초기 웹 애플리케이션과 서버가 상대적으로 단순하고 사용자 수도 적어서, 각 요청을 독립적인 쓰레드로 처리하는 것이 관리하기 쉽고 단순했기 때문이다.

    SseEmitter의 동작

    • SseEmitter도 내부적으로 비동기 서블릿을 사용한다. 즉, 쓰레드는 클라이언트의 연결이 유지되는 동안 블로킹되지 않는다.
    • 그러나 이는 "비동기"와 "논블로킹"이 같은 것은 아니라는 점을 이해해야 한다.
    • SseEmitter는 비동기적으로 작동하지만, I/O 작업 자체는 블로킹될 수 있다. 왜냐하면 기본적으로 Java의 입출력 API가 블로킹되도록 설계되었기 때문이다.

    그래서 SseEmitter의 경우, 비록 비동기 서블릿을 사용하여 클라이언트와의 연결을 유지하더라도, 내부적으로 발생하는 I/O 작업은 전통적인 블로킹 방식을 따르게 될 수 있다. 이는 SseEmitter가 Java의 기본 I/O 라이브러리를 사용하기 때문에 발생하는 제약이다.

    SseEmitter 동작 예시

    1. 클라이언트 A가 서버에 SSE(Streaming Server-Sent Events) 연결을 요청한다.
    2. 서버는 이 요청을 받아 SseEmitter 객체를 생성하고, 클라이언트 A에게 반환한다. 이 때 **SseEmitter**는 특정 쓰레드 **a**에 의해 관리된다.
    3. 쓰레드 **a**는 SseEmitter 객체와 연결된 이벤트 또는 데이터가 발생할 때까지 대기 상태로 들어간다. 이 때, 다른 작업을 처리할 수 있게 되므로 쓰레드는 여전히 활용 가능하다.
    4. 만약 다른 클라이언트 B가 다른 종류의 요청을 보낸다면, 쓰레드 a 또는 다른 가용한 쓰레드가 이를 처리할 수 있다.
    5. 서버 내에서 특정 이벤트 (예: 새로운 푸시 알림이 필요한 이벤트)가 발생하면, **SseEmitter**가 다시 활성화되어 연결된 클라이언트 A에게 데이터를 전송한다.
    6. 데이터를 전송한 후에도 **SseEmitter**와 연결은 유지되고, 쓰레드 **a**는 다시 대기 상태로 들어간다.

    이렇게 **SseEmitter**는 하나의 쓰레드를 계속 점유하는 것이 아니라, 필요한 경우에만 쓰레드를 사용하므로 비동기적으로 작동한다. 이로 인해 동시에 여러 클라이언트의 요청을 효율적으로 처리할 수 있다.

    WebFlux 동작 예시

    1. 클라이언트 A가 서버에 SSE 연결을 WebFlux를 사용해 요청한다.
    2. 서버는 이 요청을 받고, Reactor의 Flux 객체를 생성하여 클라이언트 A에게 반환한다. 이 때 쓰레드를 지정하지 않고, WebFlux의 이벤트 루프가 처리한다.
    3. 이벤트 루프는 다른 I/O 작업을 처리하거나 다른 클라이언트의 요청을 처리하는 등 여러 작업을 동시에 수행할 수 있다.
    4. 클라이언트 B가 다른 종류의 요청을 보낸다면, 이벤트 루프는 그 요청도 동시에 처리할 수 있다.
    5. 서버 내에서 특정 이벤트(예: 새로운 푸시 알림이 필요한 이벤트)가 발생하면, Flux 객체가 활성화되어 클라이언트 A에게 데이터를 전송한다.
    6. 데이터를 전송한 후에도 Flux 객체와 연결은 유지되고, 이벤트 루프는 다른 작업을 계속 처리한다.

    WebFlux는 별도의 쓰레드를 할당하지 않고 이벤트 루프를 사용하여 여러 작업을 논블로킹으로 처리한다. 이로 인해 서버의 자원을 효율적으로 활용할 수 있고, 큰 스케일의 동시 연결을 더 잘 처리할 수 있다.

    reference)

    https://perfectacle.github.io/2019/03/10/how-can-webflux-process-huge-requests-with-fewer-threads/#SpringMVC는-어떻게-동작하는가

    1. Tomcat과 Jetty의 쓰레드 풀 크기: 일반적으로 맞는 정보이다. Tomcat과 Jetty는 각각 서버 설정에서 쓰레드 풀의 크기를 지정할 수 있다.
    2. 쓰레드 생성 비용: 맞다. 쓰레드 생성은 자원을 소모하기 때문에 미리 쓰레드 풀에 쌓아놓는다.
    3. Green Thread vs Native Thread: 여기서 언급한 것은 Native Thread를 말하는 것 같다.
    4. SpringMVC 동작 방식: 설명은 대체로 맞다. I/O 블로킹 작업 발생 시 다른 요청을 처리하기 위해 새로운 쓰레드를 사용한다.
    5. WebFlux 동작 방식: 설명은 대체로 정확하다. 비동기 작업을 Queue에 넣고, 이벤트 루프가 끝난 작업을 처리한다.
    6. 쓰레드 갯수의 중요성: 신뢰성이 있는 분석이다. 쓰레드가 많을수록 동기화 문제가 복잡해진다. 또한, CPU 코어 수가 제한된 리소스이므로 그 이상의 쓰레드가 필요하지 않다.