Reactive Programming을 기반으로 구축된 Spring Framework
Netty 기반의 논블로킹 이벤트 루프 아키텍처로 동작합니다.
데이터 스트림과 변경 사항의 전파를 다루는 비동기 프로그래밍 패러다임
반응형 프로그래밍에서 비동기/논블로킹 처리를 효율적으로 다루기 위해 Reactive Streams라는 표준 명세가 만들어지고, 이 표준을 실제로 구현한 반응형 라이브러리 중 하나가 Reactor
즉, Reactor는 Java에서 Reactive Streams 명세를 실제로 구현한 라이브러리 ( 반응형 라이브러리 )
Reactive Streams의 핵심 인터페이스
Publisher : 데이터를 발행하는 역할
ex ) Mono, Flux …
Subscriber : Publisher가 전달한 데이터를 받는 소비자 역할
Subscription : Publisher ↔ Subscriber 사이의 연결( 구독 관계 ) 객체
Processor : Subscriber + Publisher 역할을 동시에 수행
Mono & Flux - Publisher
Mono와 Flux는 Reactor 라이브러리에서 제공하는 Publisher 인터페이스의 구현체
즉, Mono와 Flux는 Publisher의 한 종류로서 값을 생성하여 내보내는 발행자입니다.
Mono

0개 또는 1개의 데이터를 비동기적으로 발행하는 Publisher( 스트림 )
Flux

0개 이상의 데이터를 비동기적으로 생성하여 Publisher( 스트림 )
왜 Reactive Programming은 Mono, Flux와 같은 스트림을 반환할까
비동기 작업은 결과가 바로 준비되지 않고, 준비되는 시점에 알림을 주는 방식으로 동작합니다.
( 이 나중에 도착할 값을 표현하는 도구가 바로 Mono와 Flux )
전통적인 동기 방식에서는 값이 생성될 때까지 스레드를 블로킹한 뒤 즉시 반환하지만, 리액티브 방식에서는 스레드를 막지 않고 즉시 Mono/Flux를 반환합니다.
그리고 이 Mono/Flux는 호출한 쪽에서 구독 할 때 비로소 실행되며, 값이 준비되는 순간 해당 값을 스트림을 통해 push 형태로 전달합니다.
즉, Reactive Programming에서는 값을 직접 return 하지 않고, 미래의 값을 표현하는 스트림 Mono, Flux를 반환하는 것
Reactive Streams 호출 흐름
Reactive Streams에서 값의 흐름은 Publisher–Subscriber 패턴으로 이루어집니다.
Publisher는 새로운 값이 생성될 때마다 Subscriber에게 전달하며, 이러한 push 흐름이 반응형 프로그래밍의 핵심입니다.
[ subscribe ] - 연결 시작
Subscriber가 Publisher.subscribe(Subscriber) 를 호출하여 자신을 데이터 소비자로 등록하는 단계
( 이렇게 구독만 한 시점에는 데이터의 전송이 시작되지 않습니다. )
[ onSubscribe ] - 제어권 전달
Publisher는 Subscription 객체를 생성하여 Subscriber.onSubscribe(Subscription)를 호출합니다.
Subscription은 데이터 요청과 취소를 수행할 수 있는 주체입니다.
이 시점부터 Subscriber가 데이터 속도를 조절할 권한( Backpressure )을 획득합니다.
[ request(n) / cancel ] - 배압( Backpressure ) 조절
Subscriber는 전달받은 Subscription을 사용해 데이터를 몇 개 받을지 요청합니다.
Publisher에게 최대 n개까지 처리할 수 있다고 알리고, Publisher는 요청한 수량만큼 데이터를 전달해야합니다.[ onNext(data) ] - 데이터 전달
실제 데이터가 흐르는 단계
Publisher는 Subscriber.onNext(data)를 호출하여 데이터를 전달합니다.
오직 request(n) 범위 안이고, 데이터 스트림이 활성 상태인 동안 반복 호출 호출 가능합니다.
[onComplete / onError] - 스트림의 종료
모든 스트림은 onComplete 또는 onError 둘 중 하나로 종료되며,한 번 종료되면 다시는 onNext/onError/onComplete가 호출되지 않습니다.
onComplete
모든 데이터가 정상적으로 끝났음을 의미하는 종료 신호
반드시 subscribe → onSubscribe → request → onNext → ( onComplete/onError ) 순서로 진행되며, Subscriber가 데이터 요청을 주도하는 구조 ( Backpressure )입니다.
CF ) Controller가 Publisher를 반환하면, WebFlux가 그 Publisher에 자동으로 subscribe()하여 실제 요청 처리를 수행하여, 직접 subscribe()를 호출하지 않아도 됩니다.
Backpressure ( 배압 )
데이터를 생산하는 속도( Publisher )가 소비하는 속도( Subscriber )보다 빠를 때, Subscriber가 데이터 흐름을 조절하는 메커니즘
즉, Subscriber가 처리할 수 있을 만큼만 달라고 요청하는 것
[ Backpressure의 4가지 전략 ]
Buffering : 일정량 쌓아두고 늦게 처리
Dropping : 너무 많으면 데이터 일부 버림
Latest Only : 최신 값만 유지하고 이전 값 삭제
Error / Fail : 너무 많다라고 에러 발생시키고 스트림 종료
Reactive Streams에서 Backpressure의 본질 : 데이터는 오직 Subscriber가 request한 만큼만 Publisher가 전달해야 한다
즉, Publisher가 먼저 밀어 넣는 구조는 절대 허용되지 않습니다.
Java Blocking I/O
전통적인 Java Blocking I/O는 방식에서는 클라이언트의 요청을 처리하는 주요 메서드들이 모두 동기적으로 작동합니다.
클라이언트 연결 수락 ( ServerSocket.accept() )
서버는 ServerSocket.accept() 호출하여 커널에서 클라이언트의 연결 요청이 도착할 때까지 블로킹됩니다.
연결이 수립되면 커널은 Socket 객체를 생성하여 서버에 반환합니다.
Socket 객체 : 클라이언트와 서버의 연결 정보이자 통로
Socket 객체와 Stream
Socket 객체가 제공하는 Stream을 통해 데이터를 송수신합니다.
InputStream : 클라이언트가 보낸 데이터를 서버가 읽는 통로
OutputStream : 서버가 클라이언트에게 데이터를 보내는 통로
스트림은 단방향으로 동작하며, 실제 TCP 기반 데이터 송수신은 이 스트림을 통해 이루어집니다.
데이터 송수신

InputStream.read()을 통해 클라이언트의 데이터를 읽으며, OutputStream.write()를 통해 클라이언트에 데이터를 전송합니다.
두 메서드는 모두 작업이 완료될 때까지 스레드가 블로킹됩니다.
[ 전체 BIO 서버 흐름 ]
서버가 accept() 호출
클라이언트가 접속할 때까지 블로킹
커널이 연결을 생성하고 Socket 반환
서버는 해당 Socket의 InputStream / OutputStream 으로 통신
read() 와 write() 도 모두 블로킹
Blocking I/O의 문제는 요청마다 전용 스레드가 하나씩 필요한 Request-per-Thread 구조이기 때문에 동시 요청이 많아질수록 스레드 수가 급격히 증가하고, 이에 따른 컨텍스트 스위칭 비용도 커져 CPU 사용량이 불필요하게 높아집니다.
또한 accept(), read(), write()와 같은 I/O 작업이 완료될 때까지 스레드가 블로킹되어 다른 작업을 수행하지 못하는 것 역시 문제입니다.
Java NIO ( New Input/Output )

Java NIO는 Non-blocking을 지원하고, Selector, Channel을 도입하여 높은 성능을 보장합니다.
또한 Blocking I/O에서 스트림은 단방향으로 데이터를 처리하지만, Java NIO에서는 양방향의 Channel을 사용하여 처리합니다
[ Channel ]

Channel은 I/O 작업을 수행하기 위한 핵심 컴포넌트로, 클라이언트와 서버 사이의 연결 정보를 나타내며 그 위에서 하나 이상의 입출력 작업을 처리할 수 있습니다.
양방향 통신을 지원하며, Selector와 연동하여 단일 스레드에서 여러 채널을 관리할 수 있습니다.
ServerSocketChannel : 서버 측에서 클라이언트의 연결 요청을 처리
SocketChannel : 클라이언트와 서버 간의 데이터 송수신을 처리
위 두 Channel은 SelectableChannel를 상속하는데, 이 SelectableChannel는 아래와 같은 두 가지 메서드를 지원합니다.
configureBlocking
register
[ Selector ]

하나의 스레드가 여러 Channel의 I/O 이벤트를 감시하고 처리할 수 있게 해주는 Java NIO의 핵심 컴포넌트
이를 사용함으로써, 단일 스레드에서 여러 요청을 처리할 수 있습니다.
Selector는 자신에게 등록된 채널에서 I/O 이벤트가 발생했는지 감시하며, 이벤트가 발생한 채널을 조회하여 해당 채널에 접근할 수 있도록 해줍니다.
이는 적은 수의 스레드로 더 많은 연결을 처리할 수 있으므로 메모리 관리와 컨텍스트 전환에 따르는 오버헤드가 감소하며, 입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용할 수 있습니다.
[ Java NIO에서의 요청 처리 과정 ]
ServerSocketChannel 생성 및 논블로킹 모드 설정
서버는 ServerSocketChannel을 생성한 뒤 논블로킹 모드로 전환하여, I/O 작업에서 스레드가 블로킹되지 않도록 합니다.
Selector 생성 및 ServerSocketChannel을 OP_ACCEPT로 등록
Selector를 생성하고, ServerSocketChannel을 OP_ACCEPT 이벤트와 함께 등록합니다.
OP_ACCEPT 이벤트로 등록한다는 것은 새로운 연결이 가능한 상태가 되었을 때만 이벤트를 받고 싶다는 의미입니다.
즉, TCP 3-way handshake가 모두 완료된 상태
Selector의 select() 호출
selector.select()는 등록된 채널들 중에서 준비된 I/O 이벤트가 발생할 때까지 대기합니다.
selectedKeys()로 준비된 이벤트 조회 및 처리
selectedKeys()는 준비된 이벤트들의 목록을 Set 형태로 제공하며, 이를 iterator로 순회하면서 채널별로 필요한 처리 로직을 실행합니다.
OP_ACCEPT 처리 → SocketChannel 생성 및 등록
Selector가 OP_ACCEPT 준비됨 이벤트를 감지하면, 서버가 serverSocketChannel.accept()를 호출하여 새로운 SocketChannel이 생성됩니다. ( 요청 하나 당 SocketChannel 1개 생성 )
생성된 소켓 채널은 논블로킹 모드로 설정한 뒤, OP_READ 이벤트로 Selector에 다시 등록합니다.
이후 클라이언트가 데이터를 보낼 준비가 되면 Selector가 이를 감지하여, OP_READ 이벤트를 통해 읽기 작업을 수행할 수 있게 됩니다.
ServerSocketChannel은 서버에 하나만 존재하며, 클라이언트 요청이 들어올 때마다 이를 통해 새로운 SocketChannel이 생성하여 실제 데이터 송수신을 담당하게 합니다.
즉, NIO는 하나의 스레드가 여러 요청의 I/O 이벤트를 감시하고 처리할 수 있는 구조이며, 내부 I/O 작업도 논블로킹 방식으로 동작하기 때문에 매우 많은 요청을 효율적으로 처리할 수 있습니다.

Java 기반으로 구현된 비동기/논블로킹 방식의 Event-Driven 네트워크 애플리케이션 프레임워크
프레임워크이지만 자체적으로 서버 기능을 수행할 수 있는 구조이며, Spring WebFlux에서도 기본 내장 서버로 Netty를 사용합니다.
Netty는 이벤트 루프 모델을 사용하기 때문에, Spring WebFlux가 논블로킹 방식으로 높은 효율성과 확장성을 제공할 수 있도록 하는 기반이 됩니다.
이벤트 루프 모델은 요청마다 스레드를 생성하거나 할당하는 Tomcat과 달리, 단일 비차단 스레드로 여러 요청을 동시에 처리할 수 있는 구조를 갖습니다.
→ 최소한의 리소스로 수많은 동시 요청을 효율적으로 처리할 수 있습니다.
Event Driven
이벤트 발생에 반응하여 동작하도록 설계된 방식 ( 이벤트 발생 → 처리의 흐름 )
EventLoop Group
여러 EventLoop를 관리하는 컨테이너
BossGroup
클라이언트의 연결 요청을 수락하고, 새로운 연결을 생성한 뒤 처리를 WorkerGroup으로 위임합니다.
BossGroup은 일반적으로 하나의 EventLoop( 단일 스레드 )로 구성되며, 이 EventLoop가 ServerSocketChannel을 감시하여 OP_ACCEPT 이벤트( 연결 요청 )를 처리합니다.
WorkerGroup
실제 I/O 작업 및 채널과 관련된 이벤트를 처리합니다.
일반적으로 CPU 코어 수만큼의 EventLoop가 생성되며, 각 EventLoop( 스레드 )는 여러 Channel을 동시에 관리하고 처리합니다.
즉, BossGroup은 연결 수락 역할을 하며, WorkerGroup은 실제 비즈니스 로직과 I/O 처리를 담당합니다.
각 Channel은 WorkerGroup 내의 하나의 EventLoop에 고정적으로 바인딩되며, 해당 Channel로 들어오는 모든 I/O 이벤트는 항상 동일한 EventLoop에서 처리됩니다.
EventLoop
Netty의 핵심 구성 요소이자 실행 단위이며, 하나의 전용 스레드 + Selector + TaskQueue로 구성됩니다.
( EventLoop는 객체 )

EventLoop 객체 내에 스레드가 포함된게 아니라 바인딩 되어있는 것
Selector
EventLoop는 내부적으로 Selector가 존재하며, 이 Selector는 EventLoop에 바인딩된 Channel들의 I/O 이벤트를 감지합니다.
이후 이 이벤트를 EventLoop의 단일 스레드가 처리합니다.
TaskQueue
I/O 외의 사용자 애플리케이션 코드와 같은 일반적인 작업인 task를 저장하는 큐로, TaskQueue에 저장하였다가 EventLoop의 스레드가 순차적으로 실행합니다.
I/O 작업은 TaskQueue에 저장하여 처리하지 않고 즉시 처리합니다.

단일 스레드로 동작하는 반복 루프 구조로, 해당 스레드가 Selector를 통해 특정 Channel에서 발생한 I/O 이벤트를 감지하고 이를 내부 이벤트 큐에 저장한 뒤 순차적으로 처리합니다.
자세한 요청 처리 과정
요청이 들어오면 BossGroup의 EventLoop 스레드가 연결 요청을 accept 하여 새로운 Channel을 생성하고, 이를 WorkerGroup의 특정 EventLoop에 할당 합니다.
이후 WorkerGroup에 바인딩된 Channel에서 I/O 이벤트가 발생하면, 해당 EventLoop의 Selector가 이벤트를 감지하고, 같은 EventLoop 스레드가 해당 이벤트를 처리합니다.
처리 과정에서 이벤트는 ChannelPipeline으로 전달되며, Pipeline에 등록된 ChannelHandler들이 체인 형태로 순서대로 실행되어 요청/응답을 처리합니다.
Mono/Flux는 데이터를 생산하는 방식을 정의한 독립적인 스트림이며, 이 스트림은 subscribe()가 호출되는 순간 한 번 실행되고, 이때 체인으로 구성된 연산들이 순차적으로 동작하면서 데이터가 흐르게 됩니다.
Sink
@Slf4j
@RequiredArgsConstructor
@Service
public class SseEventService {
public static final Sinks.Many<String> sink = Sinks.many().replay().limit(1);
public Flux<ServerSentEvent<String>> streamQueueEvents(String userId, String queueType) {
return sink.asFlux()
...
}
}
Sink는 Reactor가 제공하는 이벤트 발행기로, 외부에서 데이터를 emit할 수 있도록 합니다.
return sink.asFlux()를 호출하는 이유는 Sink를 구독 가능한 Flux 형태로 만들기 위함입니다.
이렇게 반환된 Flux를 클라이언트가 구독하면, Sink로 emit되는 모든 이벤트가 이 Flux 스트림을 통해 전달되고, 이후 체인 연산들을 거쳐 최종적으로 사용자에게 데이터가 전달됩니다.
Sink를 사용하는 이유는 애플리케이션 내부에서 필요한 시점에 직접 이벤트를 발행하기 위함이며, 이를 asFlux()로 변환해 구독 가능 형태로 만든 뒤, 구독 중인 모든 클라이언트에게 데이터를 실시간으로 전달하기 위함입니다.
Sinks.Many에서 Many : 여러 개의 이벤트를 발행할 수 있는 Sink를 사용하기 위함