본문은 아래의 Reference 들을 번역하고 정리한 글입니다.
Reference
- https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367
- https://jenkov.com/tutorials/java-nio/overview.html
- https://www.alibabacloud.com/forum/read-620
- https://medium.com/geekculture/a-tour-of-netty-5020ecee5494
1. NIO(New Input/Output)
Java NIO 는 크게 3가지 컴포넌트로 이루어져 있습니다.
- Channels
- Buffers
- Selectors
추가적으로 Pipe, FileLock 과 같은 컴포넌트또한 존재하지만, 대표적인 컴포넌트는 위의 3가지라고 생각하시면 될 것 같습니다.
1.1 Channels and Buffers

NIO 는 채널로부터 시작하게 됩니다. 채널은 Stream 으로써, 해당 채널을 통해 Buffer에 값을 저장할 수 있는 것이죠. 반대로 Buffer에서 채널로 값을 쓰는것 또한 가능합니다.
1.1.1 Channels
이러한 채널은 여러 형식이 존재합니다.
- FileChannel : 파일에 데이터 쓰기/읽기
- DatagramChannel : UDP I/O 관리
- SocketChannel : TCP I/O 관리
- ServerSocketChannel : 대표적으로 서버에서 사용하는 것이죠. TCP 연결요청이 오면 이 채널에서 듣고, SocketChannel 을 만들어줍니다
위로 미루어보아 1) UDP/TCP 네트워크 IO와 2) file IO 를 바로 NIO 가 도와주게 되죠.
1.1.2 Buffers
버퍼또한 여러 형식이 존재합니다.
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
1.2 Selectors

Selector 은 여러 채널들을 싱글 스레드로 관리할 수 있도록 도와줍니다.
예를 들어 채팅 서버를 가정해볼까요? 채팅서버는 많은 수의 socket을 필요로 합니다. 실시간으로 채팅을 주고받아야 하기 때문이죠. 그렇다면 많은 채널(SocketChannel)이 열린 상태일 것이고, 해당 채널마다 각각의 스레드가 할당되면 매우매우 많은 스레드가 동시에 돌아가야겠죠? 이는 굉장히 비효율적이고 자원소모적인 과정입니다. 이를 해결하기 위해서 NIO는 Selector을 만들어 싱글 스레드로 여러 채널들을 관리할 수 있도록 하였습니다!
자, 종합하면 NIO 는 채널과 버퍼, 셀렉터를 통해 네트워크와 스토리지 입출력을 관리하도록 도와주는 Java 객체로 이해하였습니다.
2. Reactor Pattern
Reactor Pattern 은 하나 이상의 클라이언트로 부터의 I/O 요청을 동시처리하기 위해서 사용하는 패턴입니다. 해당 패턴을 알기에 앞서, 이전에 우리가 사용하던 패턴을 알아보겠습니다.
2.1 Tranditional Way of Handling Connection

보통 우리가 옛날 Tomcat을 사용하면 주로 이방식을 사용하죠. 이러한 처리방식의 문제점은 클라이언트가 늘어날 수록, 스레드가 부족하다는 점입니다. handler 마다 하나의 스레드를 잡아먹기 때문이죠. 즉, 하나의 클라이언트 별 하나의 스레드가 배정되었기 때문에 스레드 제한이 걸릴 것이고, bottleneck 이 생길 수 밖에 없습니다. 스레드 풀로 이를 완화시키긴 했지만, 여전히 스레드 제한에 걸리는 것은 피할 수 없게 됩니다. 그래서 이걸 어떻게 하면 해결할 수 있을까를 고민하게 되고 아래와 같이 Reactor 모델 이라는 것을 만들어서 아키텍처가 나왔습니다.
Reactor 모델 은 이벤트 드리븐 모델입니다. 서버가 여러개의 inbound 요청을 수신하고 이를 처리하는 핸들러와 스레드에 매핑시켜주는 모델이죠. Reactor 모델은 다른말로 Dispatcher mode 라고도 불리는데요. 이유는 I/O 멀티플렉싱을 지원하기 때문입니다. Reactor 모델의 메인 컴포넌트는 Reactor/Handler 입니다.
1) Reactor: 여러 스레드에서 동작하며, 이벤트들을 딱 맞는 핸들러에 보내주는 역할을 수행합니다. 쉽게 말하면 고객 상담센터 연결원 이라고 볼 수 있겠네요. 소비자에게 전화를 받아서 공급자에게 전달할 지 정하는 중간 매개자 역할이죠.
2) Handler: 클라이언트 요청(이벤트)을 읽고/비즈니스 로직을 수행하는 컴포넌트입니다. 쉽게 말하면, 위에서 말한 예시의 공급자 역할을 수행한다고 볼 수 있겠죠?
Reactor 모델은 리엑터 개수와 스레드 개수에 따라 3가지로 설명할 수 있습니다.
- Single-thread model (one reactor and one thread)
- Multi-thread model (one reactor and multiple threads)
- Master-slave Reactor model (multiple reactors and threads)
2.2 Single-Thread Model (one reactor and one thread)

위의 컴포넌트들을 설명해볼까요?
- Dispatcher: Reactor 내부에서 multiplexer/selector를 관리하는 모듈입니다. 이벤트가 수신되면 Acceptor가 등록한 핸들러에 매핑시켜주는 역할을 수행합니다.
- Acceptor: 클라이언트가 연결 이벤트를 전송하면 Acceptor이 수신해서 여러 타입의 핸들러를 생성하고 Dispatcher에 등록합니다.
- Handler: 실직적으로 로직을 수행하는 컴포넌트로, read-write-ready 이벤트를 처리합니다. 얘는 read, decode, process, encode, send, etc. 들을 수행하게 됩니다.
하지만 Handler 가 block 되면 모든 handler와 acceptor이 중지되는 취약점이 존재합니다. 그래서 보통 위의 모델을 사용하려면 빠르게 I/O를 하기위해 Redis로 인메모리 관리해야되죠.
2.3 Multi-Thread Model (one reactor and multiple threads)

메인 스레드에서 하나의 Reactor 는 하나의 Selector 을 통해 이벤트를 수신받고 Handler 에 이벤트를 넘겨줍니다. Acceptor 은 이벤트를 수신받을 때, Handler 를 생성하게 됩니다. 이 Handler는 오직 데이터를 읽고 쓰기만 하고, thread pool의 NIO worker thread 는 실제 비즈니스 로직을 수행하게 되죠. worker thread 는 로직 수행 이후 메인 스레드(Reactor-Handler)에 결과를 보내주고, 메인스레드의 Handler 는 클라이언트에게 결과를 Output 하게 됩니다.
종합하면 아래와 같은 순서를 거치겠군요.
- 클라이언트 요청
- Acceptor는 요청에 맞는 Handler 생성 후, Reactor에 저장
- Reactor은 클라이언트 요청을 Acceptor이 보내준 Handler에 매핑
- Handler는 클라이언트 요청을 read 하고, Thread Pool 에서 하나 꺼내서 NIO Worker Thread로써 실제 요청 비즈니스 로직 처리
- Worker Thread는 반환된 결과를 자신을 부른 Reactor의 Handler에게 전송
- Handler는 클라이언트에게 write
하지만 worker thread가 처리하는 비즈니스 로직이 너무 긴 시간이 걸린다면 어떤 문제가 발생할까요?
Reactor의 Handler를 클라이언트 요청에 따라 만들어주는 Acceptor에 bottleneck이 걸릴 거에요. Acceptor는 메인 스레드로만 동작하기 때문이죠! 뿐만 아니라 메인 스레드의 Handler들 또한 단일 스레드로만 동작하기 때문에 네트워크 입출력에서 병목이 발생합니다. 즉 비즈니스 로직을 처리하는 부분은 빠를 지 몰라도, 여전히 네트워크 연결 관리 측면에서 병목이 발생할 수 밖에 없다는 것입니다.
2.4 Master-slave Reactor model (multiple reactors and threads)

마스터-슬레이브 Reactor 모델은 위의 Multi-Thread Model 와 매우 유사합니다. 단! Acceptor과 Reactor 이 Multi-Thread인 점과 Reactor가 main과 sub로 나뉘어진것 빼곤 말이죠.
다수의 Sub-Reactor 은 각자 자신만의 selector, thread pool, dispatcher을 가지고 있습니다.
selector 는 다시한번더 정리하자면, 하나의 스레드가 여러개의 채널을 관리하도록 도와주는 녀석입니다. dispatcher 은 클라이언트 요청과 handler 를 매핑해주는 역할을 수행하구요.
Acceptor 은 Handler를 생성해서 Sub-Reactor 에다가 등록해줍니다. 그리고 하나의 Main-Reactor 은 모든 연결들을 관리해주는 역할입니다.
그럼 이제 클라이언트 요청시 내부 로직 순서를 정리해보겠습니다.
- 클라이언트 요청
- Acceptor는 요청에 맞는 Handler 생성 후, Sub-Reactor에 저장 (multi-thread)
- Main-Reactor은 클라이언트 요청을 Sub-Reactor의 Handler에 link
- Sub-Reactor의 Handler는 클라이언트 요청을 read 하고, Thread Pool 에서 하나 꺼내서 NIO Worker Thread로써 실제 요청 비즈니스 로직 처리
- Worker Thread는 반환된 결과를 자신을 부른 Sub-Reactor의 Handler에게 전송
- Handler는 클라이언트에게 write
자! 이렇게 되면, 하나의 클라이언트의 요청은 하나의 Sub-Reactor에 자연스럽게 묶이며(Main-Reactor의 link로 인해), Sub-Reactor은 단일 스레드로 네트워크 I/O를 진행하게 됩니다. 따라서 하나의 요청에 대한 state나 context와 같은 부가 정보들이 보장될 수 있게 되죠. 이전 Multi-Thread Model 에서 관찰했던 네트워크 I/O 병목현상은 여러 Sub-Reactor 과 Acceptor을 통해 해소되었습니다.
이제 Reactor I/O 모델이 끝났어요! 다음 포스팅은 이러한 리엑터 I/O 패턴을 활용한 Netty를 알아보겠습니다.
This post translates and summarizes the references below.
Reference
- https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367
- https://jenkov.com/tutorials/java-nio/overview.html
- https://www.alibabacloud.com/forum/read-620
- https://medium.com/geekculture/a-tour-of-netty-5020ecee5494
1. NIO, New Input/Output
Java NIO is mainly made of three components.
- Channels
- Buffers
- Selectors
Additional components such as Pipe and FileLock also exist, but the representative components are the three above.
1.1 Channels and Buffers

NIO starts from a channel. A channel is like a stream, and through that channel, values can be stored in a Buffer. Conversely, writing values from a Buffer to a channel is also possible.
1.1.1 Channels
There are several types of channels.
- FileChannel: write/read data to/from files
- DatagramChannel: manage UDP I/O
- SocketChannel: manage TCP I/O
- ServerSocketChannel: typically used on the server side. When a TCP connection request comes in, this channel listens and creates a SocketChannel
From this, NIO directly helps with 1) UDP/TCP network I/O and 2) file I/O.
1.1.2 Buffers
There are also several types of buffers.
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
1.2 Selectors

A Selector helps manage multiple channels with a single thread.
For example, imagine a chat server. A chat server needs many sockets because it needs to exchange chat messages in real time. Then many channels, SocketChannels, will be open. If each channel is assigned its own thread, a huge number of threads must run at the same time. This is extremely inefficient and resource-consuming. To solve this, NIO created the Selector so multiple channels can be managed with a single thread.
In summary, I understood NIO as Java objects that help manage network and storage I/O through channels, buffers, and selectors.
2. Reactor Pattern
The Reactor Pattern is used to process I/O requests from one or more clients concurrently. Before understanding this pattern, let us first look at the pattern we used before.
2.1 Traditional Way of Handling Connection

When we used old Tomcat, this was usually the approach. The problem with this handling method is that as clients increase, threads become insufficient. Each handler consumes one thread. In other words, because one thread is assigned to each client, the thread limit is eventually reached and a bottleneck is inevitable. A thread pool can mitigate it, but it still cannot avoid the thread limit. So people thought about how to solve this and came up with an architecture called the Reactor model.
The Reactor model is an event-driven model. It receives multiple inbound requests on the server and maps them to handlers and threads that process them. The Reactor model is also called Dispatcher mode because it supports I/O multiplexing. The main components of the Reactor model are Reactor and Handler.
Reactor: runs on multiple threads and sends events to the right handlers. Simply put, it is like a call center operator. It receives a call from a customer and decides which provider to deliver it to, acting as a mediator.
Handler: a component that reads client requests, or events, and performs business logic. In the example above, it is like the provider.
The Reactor model can be explained in three forms depending on the number of reactors and threads.
- Single-thread model (one reactor and one thread)
- Multi-thread model (one reactor and multiple threads)
- Master-slave Reactor model (multiple reactors and threads)
2.2 Single-Thread Model (one reactor and one thread)

Let us explain the components above.
- Dispatcher: a module inside the Reactor that manages the multiplexer/selector. When an event is received, it maps it to the handler registered by the Acceptor.
- Acceptor: when a client sends a connection event, the Acceptor receives it, creates several types of handlers, and registers them with the Dispatcher.
- Handler: the component that actually performs logic and handles read-write-ready events. It performs read, decode, process, encode, send, and so on.
But if a Handler blocks, all handlers and the acceptor stop. This is a weakness. So if this model is used, it usually needs in-memory management with something like Redis for fast I/O.
2.3 Multi-Thread Model (one reactor and multiple threads)

In the main thread, one Reactor receives events through one Selector and passes events to a Handler. The Acceptor creates a Handler when it receives an event. This Handler only reads and writes data, and the NIO worker thread from the thread pool performs the actual business logic. After the worker thread completes the logic, it sends the result to the main thread, Reactor-Handler, and the main thread’s Handler outputs the result to the client.
Summarized, the sequence is below.
- Client request
- Acceptor creates a Handler for the request and stores it in Reactor
- Reactor maps the client request to the Handler sent by Acceptor
- Handler reads the client request, takes one thread from the Thread Pool, and processes the actual request business logic as an NIO Worker Thread
- Worker Thread sends the returned result to the Reactor’s Handler that called it
- Handler writes to the client
But what happens if the business logic handled by the worker thread takes too long?
The Acceptor that creates Reactor Handlers based on client requests will become a bottleneck. That is because Acceptor only runs on the main thread. Also, the main thread’s Handlers also run on a single thread, so network I/O becomes a bottleneck. In other words, even if the business-logic processing part may be fast, network connection management still inevitably becomes a bottleneck.
2.4 Master-slave Reactor model (multiple reactors and threads)

The Master-slave Reactor model is very similar to the Multi-Thread Model above. The only differences are that Acceptor and Reactor are multi-threaded, and Reactor is divided into main and sub.
Multiple Sub-Reactors each have their own selector, thread pool, and dispatcher.
To summarize selector again, it helps one thread manage multiple channels. Dispatcher maps client requests to handlers.
Acceptor creates a Handler and registers it with a Sub-Reactor. One Main-Reactor manages all connections.
Now let us summarize the internal logic sequence when a client request arrives.
- Client request
- Acceptor creates a Handler for the request and stores it in a Sub-Reactor (multi-thread)
- Main-Reactor links the client request to the Sub-Reactor’s Handler
- The Sub-Reactor’s Handler reads the client request, takes one thread from the Thread Pool, and processes the actual business logic as an NIO Worker Thread
- Worker Thread sends the returned result to the Sub-Reactor’s Handler that called it
- Handler writes to the client
With this, one client’s request is naturally tied to one Sub-Reactor through the Main-Reactor’s link, and the Sub-Reactor performs network I/O with a single thread. Therefore, additional information such as state or context for one request can be guaranteed. The network I/O bottleneck observed in the previous Multi-Thread Model is resolved through multiple Sub-Reactors and Acceptors.
Now the Reactor I/O model is done. The next post will look at Netty, which uses this Reactor I/O pattern.