Home NIO(1)
Post
Cancel

NIO(1)

NIO에 대해 학습하며 작성된 글입니다. 학습 과정에서 작성된 글이기 때문에 잘못된 내용이 있을 수 있으며, 이에 대한 지적이나 피드백은 언제든 환영입니다.

image







1. 글을 작성하게 된 계기


이전 글에서 서블릿부터 컨트롤러 사이의 동작 과정에 대해 학습했습니다. NIO를 공부하다 보니 사용자 요청이 애플리케이션 Endpoint부터 서블릿 컨테이너까지 어떤 과정을 거쳐 동작하는지에 호기심이 생겼고, 이를 알기 위해 NIO와 연관된 개념들에 대해 학습해야 했습니다.

image







Endpoint부터 서블릿 컨테이너까지 사용자 요청이 어떻게 처리되는지를 알기 전, 이 글을 통해 NIO의 등장 배경, NIO연관 개념에 대해 살펴보겠습니다.

  1. NIO의 등장 배경
  2. NIO와 연관 개념







2. NIO


NIO(New I/O)는 Java의 전통적인 I/O 모델의 한계점을 해결하기 위해 JDK 1.4부터 도입되었습니다.

java.nio (NIO stands for New Input/Output) is a collection of Java programming language APIs that offer features for intensive I/O operations. It was introduced with the J2SE 1.4 release of Java by Sun Microsystems to complement an existing standard I/O.





전통적 Java의 I/O 모델은 스트림(Stream) 기반으로, 각 연결마다 별도의 쓰레드가 필요했습니다. 사용 가능한 쓰레드 개수는 제한이 있으므로, 많은 요청에 대해 확장성이 제한적이며, 동기 I/O 기반이었기 때문에 I/O 작업이 완료될 때까지 해당 쓰레드는 블로킹 당하는 문제가 있었습니다.

image

I/O는 파일, 네트워크 등 운영체제의 다양한 곳에서 사용되는 개념이지만, 이번 포스팅에서는 네트워크 I/O, 그중에서도 사용자 요청을 받는 관점에서 설명하겠습니다.









NIO는 이러한 문제를 해결하기 위해 등장했습니다. Non-Blocking I/O, 버퍼(Buffer) 기반 I/O, 셀렉터(Selector)를 통한 연결 관리와 같은 새로운 모델을 제공하여 한 개 또는 적은 수의 쓰레드로 다수의 요청을 효율적으로 처리할 수 있게 되었습니다. 이로 인해 애플리케이션에서의 확장성 문제를 해결하며, 자원을 효율적으로 사용할 수 있게 되었습니다.

image







3. 개념


NIO를 잘 이해하기 위해서는 버퍼(Buffer), 채널(Channel), 커넥터(Connector), 셀렉터(Selector) 등과 같은 몇 가지 개념을 알아야 하는데요, 이에 대해 살펴보겠습니다.





3-1. Buffer

버퍼는 데이터가 한 곳에서 다른 곳으로 이동하는 동안 일시적으로 데이터를 저장하는 데 사용되는 메모리의 영역입니다. NIO에서 버퍼는 채널과 상호작용할 때 사용되며, 채널에서 데이터 읽거나 쓸 수 있습니다. 채널은 파일, 네트워크 등의 I/O 소스 등과의 연결을 나타내며, 채널에서 데이터를 읽는다는 것은 I/O 소스로부터 데이터를 가져오는 것을 말합니다. 여기에는 Non-DirectBufferDirectBuffer가 있습니다.

In computer science, a data buffer (or just buffer) is a region of a memory used to store data temporarily while it is being moved from one place to another.







기존의 Java I/O는 커널 버퍼에 직접 접근할 수 없었습니다. 따라서 I/O 가 발생하면 JVM은 커널의 버퍼 영역에서 JVM 내부 메모리로 해당 데이터를 불러온 후에 접근했으며, 커널 버퍼의 데이터를 JVM 내부 버퍼로 복사하는 오버헤드가 발생했습니다. 또한 데이터 복사 중인 쓰레드는 블로킹을 당했기 때문에 작업의 효율성이 좋지 못했습니다.

image







DirectBuffer는 이런 문제를 개선하기 위해 등장했습니다. 이는 JVM 외부의 메모리에 데이터를 직접 저장하여 커널 버퍼와의 중간 복사 과정을 생략하고 데이터 처리 성능을 향상시킵니다. 이 과정에서 NIO는 블로킹 없이 I/O 작업을 수행하며, 쓰레드가 I/O 작업을 기다릴 필요가 없도록 합니다. DirectBuffer의 활용은 데이터 복사에 드는 CPU 시간을 줄이며 시스템의 효율성을 향상시킵니다.

image









NIO 내부 구현을 보면 파일을 읽고 쓰는 과정에서 DirectBuffer를 사용하는데, 이는 JVM 힙 메모리가 아닌 운영체제의 네이티브 메모리를 직접 사용하며, 이를 통해 중간 메모리 복사 과정을 생략해 시스템 호출 비용을 줄일 수 있습니다.

image

DirectBuffer는 ByteBuffer를 상속하며, byte를 사용하는 이유는 컴퓨팅에서 가장 기본적인 데이터 단위로, 운영체제와의 상호작용에서 이를 기본 단위로 사용하게 됩니다.









네이티브 I/O 연산이나 FileChannel의 read/write 연산을 할 때, 내부적으로 DirectBuffer를 사용하는데, 이를 자바 코드로 살펴보겠습니다. 코드를 보면 DirectBuffer의 자손이 아닌 경우에도 DirectBuffer를 반환하는 것을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class IOUtil {

    ......

    static int write(FileDescriptor fd, ByteBuffer src, long position,
                     boolean directIO, boolean async, int alignment,
                     NativeDispatcher nd)
        throws IOException
    {
        // DirectBuffer 타입이면 바로 반환
        if (src instanceof DirectBuffer) {
            return writeFromNativeBuffer(fd, src, position, directIO, async, alignment, nd);
        }

        ......

        ByteBuffer bb;
        if (directIO) {
            Util.checkRemainingBufferSizeAligned(rem, alignment);

            // 아니라도 DirectByteBuffer 생성
            bb = Util.getTemporaryAlignedDirectBuffer(rem, alignment);
        } else {

            // 아니라도 DirectByteBuffer 생성
            bb = Util.getTemporaryDirectBuffer(rem);
        }
        try {
            ......
            return n;
        } finally {
            Util.offerFirstTemporaryDirectBuffer(bb);
        }
    }

    ......

    public static ByteBuffer getTemporaryAlignedDirectBuffer(int size,
                                                             int alignment) {
        if (isBufferTooLarge(size)) {
            return ByteBuffer.allocateDirect(size + alignment - 1)
                    .alignedSlice(alignment);
        }

        BufferCache cache = bufferCache.get();
        ByteBuffer buf = cache.get(size);
        
        ......

        // DirectByteBuffer 생성
        return ByteBuffer.allocateDirect(size + alignment - 1)
                .alignedSlice(alignment);
    }

    ......
}









메서드를 따라가면 마지막에 DirectByteBuffer를 반환하는 것을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class ByteBuffer
    extends Buffer
    implements Comparable<ByteBuffer>
{
    ......

    // 이 부분
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }

    ......
}









즉, DirectBuffer를 사용해 중간 메모리 복사 과정을 생략하여 I/O 작업의 효율성을 향상시킨 것입니다.

image







참고로 ByteBuffer는 생성되는 위치를 기준으로 JVM Heap 내에 생성되는 HeapByteBuffer와 외부 Native 공간에 생성되는 DirectByteBuffer로 나눌 수 있습니다. HeapByteBuffer를 사용하면 JVM의 GC에 관리되기 때문에 메모리 관리로부터 비교적 자유롭지만, 이는 DMA(DirectMemoryAccess)를 활용할 수 없습니다.

image

DirectBuffer를 사용하면 JVM의 외부 메모리를 직접 다룰 수 있으므로, 큰 데이터 처리나 시스템과의 효율적인 상호작용에 유리합니다.







이런 버퍼를 사용했기 때문에 Java의 전통적인 I/O 시스템에서 사용되던 스트림의 단점을 극복하며 성능을 개선할 수 있었습니다. 스트림은 데이터의 단방향 흐름을 나타내며, 입력 스트림(InputStream)으로 데이터를 읽고 출력 스트림(OutputStream)으로 데이터를 씁니다.

A BufferedInputStream adds functionality to another input stream-namely, the ability to buffer the input and to support the mark and reset methods. …… By setting up such an output stream, an application can write bytes to the underlying output stream without necessarily causing a call to the underlying system for each byte written.







즉, 단방향으로 데이터가 흐르기 때문에 데이터를 읽고 쓰기 위해 두 개의 스트림이 필요하며, byte 단위로 데이터를 읽기 때문에 대량의 데이터를 처리할 때 이는 비효율적일 수 있습니다. NIO는 이런 스트림의 단점을 버퍼 기반의 I/O 처리 방식으로 극복했습니다. 물론 스트림에서도 버퍼를 사용하기 때문에 전통적인 Java I/O 방식의 한계를 극복했다. 정도로 이해하면 될 것 같습니다.

NIO는 버퍼를 활용하여 데이터의 입출력을 처리하는데, 데이터는 먼저 버퍼에 저장되며, 이 버퍼를 통해 한 번에 여러 바이트의 데이터를 읽거나 쓸 수 있습니다. 이로 인해 데이터의 위치 이동 없이 필요한 부분만을 효율적으로 읽고 쓸 수 있게 되어 성능 개선의 이점을 가져옵니다.







10만 개의 데이터를 쓰는 간단한 예제로 성능 차이를 보면, 약 10배 이상의 효율이 차이 나는 것을 볼 수 있습니다. 이에 관해 볼 만한 글이 있는데, 한 번 참조해 보실 것을 권장해 드립니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {

    public static long nonBufferIO() {
        long start = System.currentTimeMillis();
        for (int index = 0; index <= 100_000; index++) {
            System.out.println(index);
        }
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
        return end - start;
    }

    public static long bufferIO() {
        long start = System.currentTimeMillis();
        StringBuffer stringBuffer = new StringBuffer();
        for (int index = 0; index <= 100_000; index++) {
            stringBuffer.append(index);
        }
        System.out.println(stringBuffer.append("\n"));
        long end = System.currentTimeMillis();
        System.out.println((end - start) + "ms");
        return end - start;
    }
}
1
2
Non-Buffer: 195ms
Buffer: 17ms







3-2. Channel

채널(Channel)은 데이터가 흐르는 양방향 통로로, 데이터의 효율적인 입/출력을 위해 도입된 개념입니다. 이는 파일, 네트워크 등의 I/O 소스와의 연결을 나타내며, 데이터의 양방향 흐름을 지원해서 하나의 채널을 통해 동시에 읽기와 쓰기 작업을 수행할 수 있습니다.

A nexus for I/O operations. A channel represents an open connection to an entity such as a hardware device, a file, a network socket, or a program component that is capable of performing one or more distinct I/O operations, for example reading or writing.





사용자로부터 온 요청은 채널을 통해 버퍼로 전송되며, 이는 비동기로 처리되기 때문에 데이터를 읽거나 쓸 때 쓰레드가 차단되지 않습니다. 또한 버퍼에서 버퍼로 복사되는 불필요한 과정이 줄어들어 성능상 이점을 가져갈 수 있게 된 것입니다. 이는 zero-copy라고도 부릅니다.

image







채널은 비동기 I/O를 지원하기 때문에 단일 ByteBuffer에서도 여러 작업을 동시에 처리할 수 있고, DirectBuffer를 통해 데이터 처리 성능을 향상시켜 빠르고 효율적인 I/O 작업을 가능하게 합니다. 또한 Selector와의 연동으로 여러 I/O 연결을 한 ByteBuffer에서 효율적으로 관리할 수 있어, 작업의 효율성을 높여줍니다.

image







채널에는 여러 유형이 있는데, FileChannel은 파일 I/O를, SocketChannelServerSocketChannel은 TCP 네트워크 통신을, 그리고 DatagramChannel은 UDP 통신을 위해 사용됩니다. 이 중 앞으로 다루게 될 채널은 SocketChannel입니다.

1
2
3
4
5
6
public abstract class FileChannel
    extends AbstractInterruptibleChannel
    implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
    ......
}
1
2
3
4
5
6
public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
{
    ......
}
1
2
3
4
5
public abstract class DatagramChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
{
}

Native I/O: 채널은 MMIO(Memory Mapped I/O)와 같은 네이티브 I/O, File Locking 같은 기능을 사용할 수 있습니다.







File I/O에서도 살펴볼 점이 있는데, 이는 기본적으로 Non-Blocking 방식으로 동작하지만, Blocking 방식도 지원한다는 점입니다. File I/O에 사용되는 newBufferedReader( ), newInputStream( ), newBufferedWriter( ), newOutputStream( ) 메서드는 블로킹 방식 입니다. 이는 ReadableByteChannel/WritableByteChannel의 구현체로, 이를 (구현)상속하는 모든 구현체는 블로킹 방식으로 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * A channel that can read bytes.
 *
 * <p> Only one read operation upon a readable channel may be in progress at
 * any given time.  If one thread initiates a read operation upon a channel
 * then any other thread that attempts to initiate another read operation will
 * block until the first operation is complete.  Whether or not other kinds of
 * I/O operations may proceed concurrently with a read operation depends upon
 * the type of the channel. </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface ReadableByteChannel extends Channel {

    ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * A channel that can write bytes.
 *
 * <p> Only one write operation upon a writable channel may be in progress at
 * any given time.  If one thread initiates a write operation upon a channel
 * then any other thread that attempts to initiate another write operation will
 * block until the first operation is complete.  Whether or not other kinds of
 * I/O operations may proceed concurrently with a write operation depends upon
 * the type of the channel. </p>
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */

public interface WritableByteChannel
    extends Channel
{
    ......
}







비록 이는 블로킹 방식으로 동작하지만 데이터는 버퍼를 통해 이동하므로 기존의 java.io 패키지에서 사용하는 Stream I/O에서 병목을 유발하는 몇 가지 레이어를 건너뛸 수 있어 성능상의 이점을 누릴 수 있는 것입니다.

The java.nio.file package supports channel I/O, which moves data in buffers, bypassing some of the layers that can bottleneck stream I/O.







물론 파일 I/O에서도 논 블로킹이 사용되는데, NIO2이 AsynchronousFileChannel이 대표적입니다.

1
2
3
4
5
public abstract class AsynchronousFileChannel
    implements AsynchronousChannel
{   
    ......
}

이 외에도 Scatter/Gather 등과 같은 추가로 알아야 할 개념이 있지만, 이는 별도의 포스팅에서 다루겠습니다.







3-3. Connector

클라이언트로부터의 요청을 받아 그 요청을 웹 애플리케이션 서버가 이해할 수 있는 형태로 변환해 주는 역할을 합니다.

  1. 클라이언트 요청 수신: 클라이언트로 부터 오는 TCP 요청 수신.
  2. Socket 연결: 수신된 요청은 특정 소켓에 바인딩 되어, 이를 통해 클라이언트와 서버 사이의 통신이 이루어 집니다.
  3. 패킷 파싱: 수신된 패킷을 파싱해 HTTP 요청 정보를 추출.
  4. HttpServletRequest 생성: 파싱된 HTTP 요청 정보를 토대로 서블릿 객체를 생성.
  5. ServletContainer로 전달: 이는 서블릿 컨테이너로 전달되어 처리.







즉, Tomcat 서버에서 커넥터는 클라이언트와의 통신을 처리하는 구성 요소로, 클라이언트의 요청을 Servlet이 처리할 수 있는 HttpServletRequest로 바꿔서 서블릿 컨테이너에 전달해 주는 역할을 하는 것입니다.

image







커넥터에는 여러 유형이 있으며, 그중 가장 대표적인 두 가지는 HTTP ConnectorAJP 커넥터입니다.

  1. HTTP Connector: Tomcat의 기본 커넥터로, HTTP/1.1 프로토콜로 클라이언트의 요청을 수신합니다. Tomcat을 독립 실행형 서버로 사용할 때 활용되며, 특정 TCP 포트에서 연결 요청을 수신하여 각 요청을 별도의 쓰레드로 처리합니다.

  2. AJP(Apache JServ Protocol) Connector: AJP 프로토콜을 구현하여 Tomcat을 다른 웹 서버와 연동시키기 위해 사용됩니다. 이는 binary 통신 방식을 통해 웹 서버와 통신하며, 웹 서버가 정적 컨텐츠를 처리하고 동적 컨텐츠 처리를 Tomcat에게 위임하는 구성에서 주로 활용됩니다.







이는 다수의 클라이언트와의 동시 연결을 처리하기 위한 다양한 속성을 설정할 수 있습니다.

  1. port: 포트 번호
  2. accept-count: 동시에 처리할 수 있는 연결 요청의 최대 큐 크기
  3. acceptor-thread-count: 연결 수락에 사용될 쓰레드 수
  4. connection-timeout: 클라이언트로부터 데이터를 기다리는 시간(m/s)
  5. max-connections: 한 번에 서버가 처리할 수 있는 최대 연결 수
  6. max-threads: 동시에 처리할 수 있는 최대 요청 처리 쓰레드 수







이는 스프링의 application.yml에서 설정할 수 있습니다. 여기에는 BIO ConnectorNIO Connector 두 가지 개념이 존재하는데, 이에 대해 살펴보겠습니다.

1
2
3
4
5
6
7
8
server:
  port: 8090
  tomcat:
    accept-count: 1
    max-connections: 1
    threads:
      max: 200
      min-spare: 200







3-3-1. BIO Connector

BIO(Blocked I/O) Connector는 클라이언트의 요청이 들어올 때마다 새로운 쓰레드를 생성하거나 쓰레드 풀에서 하나를 가져와 연결을 처리합니다. 이는 동시에 많은 요청이 발생할 경우 대응하기 위해 많은 쓰레드를 생성하거나 유지해야 하는 문제점이 있으며, 각 연결이 유휴 상태일 때도 쓰레드가 그 연결에 계속 할당되어 있어 자원 소모가 큽니다. 유휴 상태는 데이터 전송이 없더라도 해당 쓰레드가 연결에 계속 바인딩 되어 다른 작업을 수행할 수 없는 상태를 말합니다. 이는 다음과 같은 과정을 거칩니다.

  1. 소켓 생성: 클라이언트의 요청이 들어오면, Acceptor는 포트 리스너를 통해 해당 요청을 위한 소켓을 생성
  2. 워커 쓰레드 할당: 소켓은 워커 쓰레드 풀의 쓰레드에 작업 할당
  3. Handler: 워커 쓰레드는 Http11ConnectionHandler를 통해 Http11Processor 객체를 가져와 요청을 처리
  4. Request 객체 생성: Http11Processor 객체는 CoyoteAdapter를 통해 HTTP 요청을 HttpServletRequest로 변환
  5. 컨테이너에 전달: 서블릿 컨테이너에 요청 전달.







이를 간략히 그림으로 나타내면 다음과 같습니다.

image







클라이언트의 요청이 들어오면 Acceptor는 해당 포트에서 연결을 수락하고 소켓을 생성합니다. 이 소켓은 사용 가능한 워커 쓰레드(Worker Thread)에 할당됩니다. 할당된 워커 쓰레드는 Http11ConnectionHandler를 통해 Http11Processor 객체를 전달받으며, Http11Processor는 CoyoteAdapter를 사용해 HTTP 요청을 서블릿 요청 명세에 맞게 HttpServletRequest로 변환합니다. 변환된 요청은 적절한 서블릿으로 전달되어 처리됩니다.

image







3-3-2. NIO Connector

NIO Connector는 Non-Blocking 방식을 사용해서 한 쓰레드가 여러 연결을 동시에 처리할 수 있으며, 한 쓰레드가 요청을 처리하는 동안 다른 쓰레드가 블로킹 되지 않습니다. 이를 통해 유휴 상태의 연결이 쓰레드 자원을 차지하지 않게 되었으며, 쓰레드 사용의 효율성과 시스템의 확장성이 향상시켰습니다. Poller라는 단일 쓰레드가 여러 연결을 관리하며 Selector를 사용하여 읽기/쓰기가 가능한 연결을 찾아냅니다. 이는 다음과 같은 순서로 동작합니다.

  1. 요청 수락: Acceptor가 클라이언트의 연결 요청을 수락.
  2. 객체 변환: 연결된 소켓을 PollerEvent 객체로 변환.
  3. 큐에 추가: 변환된 PollerEvent 객체를 PollerEvent Queue에 추가.
  4. 채널 모니터링: Poller thread는 Selector를 사용하여 여러 채널의 상태를 모니터링.
  5. 채널에 할당: 채널이 데이터를 읽을 준비가 되면, Poller는 적절한 worker thread를 찾아 해당 채널(소켓)을 해당 ByteBuffer에 연결합니다.
  6. 요청 처리 :worker thread는 요청을 처리하고, 처리 결과를 클라이언트에게 소켓을 통해 응답합니다.





이는 다음과 같이 동작합니다. 이는 동작 과정에서 상세히 살펴볼 예정이기 때문에 지금은 잠시 넘어가겠습니다.

image







이 중 Poller에 대해서 잠깐 살펴보겠습니다. Poller는 이벤트 큐(Event Queue)에서 이벤트를 가져와 실질적인 데이터 처리를 위한 준비를 합니다. 내부적으로 Selector 객체를 활용하여 데이터를 읽을 준비가 된 소켓을 선택하고, 이 소켓을 Worker 쓰레드로 전달합니다.

image







이를 자바 코드로 보면 다음과 같습니다. Acceptor에서 받은 이벤트를 register( ) 메서드를 통해 등록하고, 데이터 처리가 가능한 순간에만 적절한 쓰레드를 할당합니다. 이를 통해 대기 중인 쓰레드를 적게 만들어 리소스를 효율적으로 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {

    ......

    public void register(final NioSocketWrapper socketWrapper) {
        socketWrapper.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
        PollerEvent pollerEvent = createPollerEvent(socketWrapper, OP_REGISTER);
        addEvent(pollerEvent);
    }

    ......

}







이벤트는 Poller 클래스의 events( ) 메서드에서 반복문을 순회하며 처리됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Poller implements Runnable {
    
    ......

    public boolean events() {
            boolean result = false;

            PollerEvent pe = null;

            // 이벤트 처리
            for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
                result = true;
                NioSocketWrapper socketWrapper = pe.getSocketWrapper();
                SocketChannel sc = socketWrapper.getSocket().getIOChannel();
                int interestOps = pe.getInterestOps();
                if (sc == null) {
                    log.warn(sm.getString("endpoint.nio.nullSocketChannel"));
                    socketWrapper.close();
                } else if (interestOps == OP_REGISTER) {
                    try {
                        sc.register(getSelector(), SelectionKey.OP_READ, socketWrapper);
                    } catch (Exception x) {
                        log.error(sm.getString("endpoint.nio.registerFail"), x);
                    }
                } else {

                }
            }
    }

    ......
}







BIO와 NIO에 대한 스펙/버전 정보는 다음과 같습니다.

image







3-4. Selector

Selector는 NIO에서 다수의 네트워크 채널의 I/O 이벤트를 효율적으로 감지하는 클래스입니다. 이를 통해 하나의 쓰레드로 여러 채널의 상태 변화를 관리할 수 있습니다. Selector는 운영 체제의 커널 레벨 이벤트 알림 기능을 활용하여 I/O 상태의 변화를 모니터링합니다.

A multiplexor of SelectableChannel objects. A selector may be created by invoking the open method of this class, which will use the system’s default selector provider to create a new selector. A selector may also be created by invoking the openSelector method of a custom selector provider. A selector remains open until it is closed via its close method.





즉, 소켓이 생성되면 register를 통해 Selector에 등록하고, 비동기로 쓰레드를 할당하는 것입니다. 이를 통해 적은 수의 쓰레드로 블로킹 없이 최대한의 효율을 낼 수 있습니다.

image







채널의 상태 변화를 관리한다는 표현은 여러 채널 중 I/O 작업을 수행할 준비가 된 채널들을 선택하고, 해당 채널에 작업을 할당하는 것을 말합니다. 이는 여러 소켓의 상태 변화를 동시에 감지하는데, 운영 체제의 select, poll 또는 epoll과 같은 시스템 콜을 이용합니다. 이를 통해 하나의 쓰레드만으로 여러 연결을 효율적으로 처리할 수 있어 자원을 효율적으로 사용할 수 있습니다.

image







Selector의 동작 순서는 다음과 같습니다. 상세한 동작 순서는 동작 과정에서 살펴보겠습니다.

  1. Channel 생성 및 등록: 채널 생성 및 생성된 채널 Selector에 등록. 이때 SelectionKey 생성.
  2. 이벤트 감시: 등록된 모든 채널에 대해 발생 가능한 I/O 이벤트 감시. 이벤트가 감지되면 SelectionKey 상태 변경.
  3. 이벤트 처리: I/O 이벤트가 발생하면 해당 이벤트 처리.







이를 감시하는 것은 멀티 플렉싱(Multiplexing)을 통해 이루어지는데, 그중에서도 I/O 멀티플렉싱이 사용됩니다. 이는 여러 I/O 연결을 동시에 모니터링하는 방법입니다.

We want to be notified if one or more I/O conditions are ready (i.e., input is ready to be read, or the descriptor is capable of taking more output). This capability is called I/O multiplexing and is provided by the select and poll functions, as well as a newer POSIX variation of the former, called pselect.







멀티플렉싱(multiplexing)은 여러 개의 신호나 데이터 스트림을 하나의 채널을 통해 동시에 전송하는 과정을 말하는데, 하나의 채널을 통해 동시에 여러 스트림을 관리할 수 있기 때문에 자원 최적화, 대역폭 관리 등의 이점을 얻을 수 있습니다.

In telecommunications and computer networking, multiplexing (sometimes contracted to muxing) is a method by which multiple analog or digital signals are combined into one signal over a shared medium. The aim is to share a scarce resource – a physical transmission medium.







이 모든 것들은 Non-Blocking을 기반으로 동작하는데, 이에 대해서는 별도로 다루지 않겠습니다. 지금까지 NIO의 핵심 개념들에 대해 살펴보았는데, 이를 조합해 어떻게 NIO가 동작하는지 코드 레벨에서 알아보겠습니다.

In computer science, asynchronous I/O (also non-sequential I/O) is a form of input/output processing that permits other processing to continue before the transmission has finished. A name used for asynchronous I/O in the Windows API is overlapped I/O.









4. 정리


NIO의 등장배경NIO, 그리고 버퍼(Buffer), 채널(Channel), 커넥터(Connector), 셀렉터(Selector)와 같은 연관 개념들에 대해 살펴보았습니다. 다음 글에서는 해당 개념들을 조합해서 어떻게 사용자 요청이 서블릿 컨테이너에 도달하는지를 살펴보겠습니다.

image


This post is licensed under CC BY 4.0 by the author.