Spring/Framework 탐방 zone

[Netty] Netty가 뭐에요? - 부록2: NIO 는 어떻게 비동기를 구현했을까?

공대키메라 2026. 2. 1. 19:21

최근에 글을 계속 다시 정리하고 있는데, 문득 생각이 났다.

 

동일한 코드인데 어떻게 Java NIO는 비동기를 무엇을 통해서 구현했는지 궁금했다.

 

이에 대한 분석 및 이해를 하기 위한 글을 이번 섹션에서 정리한다.

 


목표

 

0. 동기와 비동기 그리고 블로킹과 논블로킹 차이점 재정리

1. 비동기를 어떻게 구현하지?

2. 직접 구현시의 문제점


0. 동기 vs 비동기 & 블로킹 vs 논블로킹 차이점 재정리

너무 자주 봤지만, 맨날 까먹는... 이 내용을 명확하게 다시 집고가자.

 

해당 내용은 Gemini에게 요청해서 내가 다듬도록 했다.

 

0.1) 동기와 비동기 (Synchronous vs Asynchronous)

핵심 기준: "요청한 결과값을 누가, 언제 신경 쓰는가?"

 

- 동기 (Synchronous)

  • 요청자(Caller)가 요청을 보낸 후, 결과가 나올 때까지 계속 기다립니다.
  • 결과가 반환되어야만 다음 작업을 수행합니다.
  • 예: 전화 통화 (상대방이 받을 때까지 대기하고, 말이 끝나야 내가 말함)

 

- 비동기 (Asynchronous)

  • 요청자(Caller)가 요청만 보내고, 결과를 기다리지 않고 바로 다른 작업을 수행합니다.
  • 작업이 끝나면 콜백(Callback) 함수 등을 통해 결과를 통보받습니다.
  • 예: 이메일 전송 (보내놓고 다른 일 하다가, 답장이 오면 알림 받음)

 

0.2) 블로킹과 논블로킹 (Blocking vs Non-Blocking)

핵심 기준: "제어권(Thread)을 누가 가지고 있는가?"

 

 

- 블로킹 (Blocking)

  • 호출된 함수가 작업을 마칠 때까지 제어권을 가지고 놓아주지 않음 (호출자는 아무것도 못하고 대기).

 

- 논블로킹 (Non-Blocking)

  • 호출된 함수가 바로 제어권을 호출자에게 돌려줌 (호출자는 기다리지 않고 자기 할 일을 계속함).

 

명확한 상황에 대해 제미나이에에 질문을 해봤다. 궁금하면 열어보면 된다.

더보기

나: 동기와 비동기, 그리고 블로킹과 논블로킹. 이 개념들이 비슷해서 자꾸 헷갈리는데, 어떻게 구분하면 명확할까?

제미나이: 가장 확실한 구분법은 **'관심사(Focus)가 어디에 있는가'**를 보는 것입니다.

동기 vs 비동기 (결과값에 대한 관심)동기(Synchronous): 호출자(Caller)가 요청을 보낸 뒤, 결과가 나올 때까지 계속 신경 쓰고 기다리는 것입니다.

비동기(Asynchronous): 요청을 보낸 뒤 결과를 기다리지 않고, 나중에 **완료되었다는 신호(콜백)**를 받아서 처리하는 것입니다.블로킹 vs 논블로킹 (제어권/스레드에 대한 관심)

블로킹(Blocking): 호출된 함수가 작업을 마칠 때까지 제어권을 쥐고 놓아주지 않는 것입니다. (호출자는 그동안 아무것도 못 하고 대기)

논블로킹(Non-blocking): 호출된 함수가 제어권을 즉시 반환하는 것입니다. (호출자는 기다리지 않고 바로 자기 할 일을 할 수 있음)

[Netty의 핵심] Netty가 고성능인 이유는 **비동기(결과를 나중에 처리)**이면서 동시에 논블로킹(I/O 작업 중에도 스레드가 멈추지 않음) 방식을 채택했기 때문입니다.

---

나: 4가지 상황에 대해 설명해줄래? 2 x 2 가 각자 엮인 상황이 있자나.

제미나이: 가장 이해하기 쉬운 '카페 주문' 상황으로 4가지를 딱 정리해 드릴게요.

[1] 동기 + 블로킹 (Sync + Blocking)
> "전통적인 옛날 카페" - 상황: 주문 후 커피가 나올 때까지 카운터 앞에서 꼼짝 않고 서서 기다립니다.
-특징: 점원(시스템)도 손님을 못 가게 잡고 있고, 손님(나)도 결과가 나올 때까지 아무것도 안 하고 쳐다보고만 있습니다. (가장 흔한 옛날 방식)

[2] 동기 + 논블로킹 (Sync + Non-blocking)
> "진동벨 없는 카페의 성격 급한 손님"
- 상황: 주문 후 자리에 앉았지만(Non-blocking), 진동벨이 없어서 1분마다 카운터로 가서 "나왔어요?" 하고 계속 물어봅니다.
- 특징: 몸은 자유롭지만, 결과가 궁금해서 계속 확인하느라(Sync) 정작 내 할 일(유튜브 보기 등)에 집중을 못 합니다. (CPU 낭비)

[3] 비동기 + 논블로킹 (Async + Non-blocking)
> "스타벅스 (진동벨/사이렌오더) - Netty의 방식"
- 상황: 주문 후 진동벨을 받고 자리에 가서 편하게 내 할 일을 합니다. 진동벨이 울리면(Callback) 그때 가서 받습니다.
- 특징: 몸도 자유롭고(Non-blocking), 결과도 신경 끄고 있다가 알림이 오면 처리하므로(Async) 가장 효율적입니다.

[4] 비동기 + 블로킹 (Async + Blocking)
> "진동벨 받고 카운터 앞에서 멍때리기 (안티 패턴)"
- 상황: 점원이 "가서 기다리세요" 하고 진동벨을 줬는데(Async), 굳이 진동벨을 꽉 쥔 채 카운터 앞에서 서서 기다립니다(Blocking).
- 특징: 비동기 기술을 써놓고도 마지막에 강제로 기다리게 만드는(예: Future.get()) 실수입니다. 좋은 기술 써서 성능 깎아먹는 케이스죠.

 

땡큐 제미나이!

 

다시 정리하자면 어느것에 집중하느냐로 나뉜다. 

 

동기 vs 비동기는 요청한 결과값에 집중하고, 블로킹 vs 논블로킹은 제어권(Thread) 에 집중한다.

 

동기는 요청 결과를 계속 기다라고, 비동기는 요청 후 결과를 기다리지 않고 다른 일을 한다.

 

블로킹은 요청을 하면 제어권을 넘기지 않고 잡고 있지만, 논블로킹은 제어권을 바로 요청자에게 넘긴다.

 

논블로킹을 사용하면, 쓰레드 하나가 작업이 끝나길 기다리지 않고 제어권을 바로 돌려받으니, 뒤에서 작업을 하는 동안 다른 요청을 받아서 처리가 가능하다.

 

[정리]

  • 논블로킹 (행동): "지금 당장 멈추지 않고 움직일 수 있는가?" (Yes, 전화를 바로 끊어주니까)
  • 비동기 (결과): "결과가 나왔을 때 나한테 알려주는가?" (No, 안 알려줘서 내가 계속 확인해야 함)

 

[요약]

  • 비동기는 "완료됐어! (알림)"가 핵심
  • 논블로킹은 "지금 가서 딴 거 해! (반환)"가 핵심

 

1. 비동기를 어떻게 구현하지?

여태 Netty 관련해서 공식문서를 읽어보았는데, Java NIO 기능을 이용해서 Netty에서 편하게 쓸 수 있게 지원을 하고 있다.

 

원래 프레임워크의 목적이 그렇지 않은가? 뛰어난 기능을 쉽게 쓸 수 있게 지원을 해주는것 말이다.

 

여기서 필자가 궁금한 부분은 어느 코드에서 도대체 어떻게 비동기적으로 기능할 수 있도록 하는가 하는 부분이었다.

 

이에 대한 탐구를 해나갈 것이다. 

 

1.1) 예시 코드

RawNioServer.java

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

public class RawNioServer {

    public static void main(String[] args) throws IOException {
        // [추적 포인트 1] Selector.open()
        // -> 내부에서 OS를 확인하고 EPollSelectorProvider(Linux) 또는 KQueueSelectorProvider(Mac)를 로딩합니다.
        Selector selector = Selector.open();

        // ServerSocketChannel 생성 (아직은 블로킹 모드)
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        serverSocket.bind(new InetSocketAddress(8080));

        // [추적 포인트 2] configureBlocking(false)
        // -> 이 메서드를 타고 들어가면 OS 시스템 콜을 통해 소켓의 FD(File Descriptor) 속성을 변경합니다.
        // -> "이 소켓은 이제부터 I/O 작업 시 대기하지 않는다"라고 선언.
        serverSocket.configureBlocking(false);

        // [추적 포인트 3] register()
        // -> OS의 감시 목록(epoll_ctl 등)에 이 소켓을 등록합니다.
        // -> "OP_ACCEPT(연결 요청)가 오면 알려줘"
        serverSocket.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("NIO Server Started on port 8080...");

        while (true) {
            selector.select();

            // [수정 1] 이벤트 발생한 애들 말고, '전체 등록된 목록'을 보고 싶다면 keys()를 써야 합니다.
            Set<SelectionKey> allKeys = selector.keys();
            System.out.println("--- 현재 관리 중인 전체 채널 (" + allKeys.size() + "개) ---");
            for (SelectionKey key : allKeys) {
                if (!key.isValid()) continue; // 연결 끊긴 애들은 건너뛰기

                Channel channel = key.channel();
                if (channel instanceof ServerSocketChannel) {
                    System.out.println(" - [서버 소켓]: 8080 포트 감시 중");
                } else if (channel instanceof SocketChannel) {
                    SocketChannel sc = (SocketChannel) channel;
                    // 연결된 상태일 때만 주소 찍기
                    if (sc.isConnected()) {
                        System.out.println(" - [클라이언트]: " + sc.getRemoteAddress());
                    }
                }
            }
            System.out.println("--------------------------------------------------");

            // 이벤트 처리 시작
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                iter.remove();

                if (!key.isValid()) continue;

                try {
                    if (key.isAcceptable()) {
                        handleAccept(serverSocket, selector);
                    } else if (key.isReadable()) {
                        handleRead(key);
                    }
                } catch (IOException e) {
                    // 예외 발생 시 해당 키(연결) 취소 및 소켓 닫기
                    key.cancel();
                    try { key.channel().close(); } catch (IOException ex) {}
                    System.out.println("!! 연결 오류로 소켓 종료 !!");
                }
            }
        }
    }

    private static void handleAccept(ServerSocketChannel serverSocket, Selector selector) throws IOException {
        SocketChannel client = serverSocket.accept();
        System.out.println("Client connected: " + client.getRemoteAddress());

        // ★ 중요: 클라이언트 소켓도 반드시 논블로킹으로 설정해야 함
        client.configureBlocking(false);

        // 클라이언트 소켓을 Selector에 등록 (이제부터 이 클라이언트가 보내는 데이터(READ)를 감시)
        client.register(selector, SelectionKey.OP_READ);
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(256);

        int bytesRead;
        try {
            bytesRead = client.read(buffer);
        } catch (IOException e) {
            // [수정 2] 읽다가 에러 나면(강제 종료 등) 여기서 잡아서 처리
            System.out.println("손님이 연결을 강제로 끊었습니다: " + client.getRemoteAddress());
            client.close();
            return;
        }

        if (bytesRead == -1) {
            client.close();
            System.out.println("Client disconnected (정상 종료)");
        } else if (bytesRead > 0) {
            buffer.flip();
            // 받은 데이터 문자열로 확인해보기 (디버깅용)
            String msg = new String(buffer.array(), 0, bytesRead);
            System.out.println("[데이터 수신]: " + msg.trim());

            client.write(buffer); // 에코
            buffer.clear();
        }
    }
}

 

해당 코드는 필자가 추적을 하기 쉽게 제미나이에게 예시 코드를 작성해달라고 했다.

 

 

1.2) 구성도 리뷰 및 흐름 추적

Java NIO 에 대해 간단하게 논의한 내용이 이전에 있다. 궁금하면 봐주시면 감사하무니다! (여기 클릭)

 

 

큰 구조는 하나의 스레드가 하나의 selector를 관리하고, selector는 여러개의 채널을 관리한다. 

 

그럼 실제로 이렇게 돌아가는지 디버깅 해보려고 한다. break point를 걸어보자!

 

 

우선 Selector를 설정한다.

 

open Selector의 static method 인 open()을 호출한다.

 

 

다시 SelectorProvider의 provider()를 호출하고 나서 openSelector() 메소드를 호출한다.

 


synchronized 되있는 것과, null이 아님을 체크하는 것을 보니 뭔가 싱글톤 패턴같다.

 

로직을 거쳐서 SelectProvider를 생성한다.

 

loadProviderFromProperty() 메소드 내부적으로... 뭔가 체크를 하는데, 필자는 값이 null이기에 다음으로 넘어간다

 

null이라서 다음인 loadProviderAsService() 로 넘어건다.

 

물론 여기서도 값이 없어서 바로 false를 반환해서 결국엔 가장 마지막 메소드에서 create를 해준다.

 

여기서 재미있는 부분은 sun.nio.ch.DefaultSelectorProvider.create()를 하면 

 

오잉? WindowSelectorProvider() 가 있다. 

 

이게 어떻게 내 OS 가 window인것을 안거지? 하는 의문을 가지지만... 우선 쭉 읽어보도록 하겠다.

 

하여간 provider가 없는 상태에서 우리는 WindowSelectorProvider를 받아냈다.

 

이 provider로 openSelector() 메소드를 호출한다.

 

 

WindowsSelectorImpl을 살펴보면 다음과 같다.

 


WindowSelectorProvider를 넘겨받은 WindowSelectorImpl 은 부모 생성자 호출 후 여러 변수들을 세팅한다.

 

pollWrapper, PollArrayWrapper, Pipe, wakeupPipe, AllocateNativeObject 등등...

 

정말 많은 메소드를 호출해서 작업을 다 하고, 이러한 필요한 정보들이 세팅이 된 Selector가 반환된다.

 

그 받을 Selector하나로 ServerSocketChannel로 활용한다. 

 

 

여기서 의문점은 이 코드를 뒤져보니 코드 레벨에서 OS를 파악하는건 없는데 어떻게 내가 Window인지 알고 WindowSelectorProvider를 제공했는지 궁금했다. 

 

찾아보니 jdk를 다운받을 때 어느 os 에서 쓸 지 미리 알고 다운로드를 하기에 특정 os 에 맞추지 않나?

 

필자의 경우에는 window용 jdk를 받았을거니 default는 당연히 Window라는 것이다.

 

그다음 어느 port 로 서버를 작동시킬것인지 설정한다.

 

 

그리고 serverSocket에서 configureBlocking 설정은 false로 한다.

 

 

 

여기서 implConfigureBlocking(block) 이 있는데, 이것이 blocking 옵션을 설정하는 부분이라고 한다.

 

 

IOUtil을 보자. 그러면.... native 코드라네~

 

 

해당 native code에 대해서 분석하고자 github 에서 찾아서 코드를 긁어왔다. (원본 주소)

 

궁금하면 펼쳐보자! 

더보기
/*
 * Copyright (c) 2000, 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

#include <windows.h>
#include <winsock2.h>
#include <io.h>
#include "jni.h"
#include "jni_util.h"
#include "jvm.h"
#include "jlong.h"

#include "java_lang_Long.h"
#include "nio.h"
#include "nio_util.h"
#include "net_util.h"
#include "sun_nio_ch_IOUtil.h"

/* field id for jlong 'handle' in java.io.FileDescriptor used for file fds */
static jfieldID handle_fdID;

/* field id for jint 'fd' in java.io.FileDescriptor used for socket fds */
static jfieldID fd_fdID;

JNIEXPORT jboolean JNICALL
Java_sun_security_provider_NativeSeedGenerator_nativeGenerateSeed
(JNIEnv *env, jclass clazz, jbyteArray randArray);

/**************************************************************
 * static method to store field IDs in initializers
 */

JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_initIDs(JNIEnv *env, jclass clazz)
{
    CHECK_NULL(clazz = (*env)->FindClass(env, "java/io/FileDescriptor"));
    CHECK_NULL(fd_fdID = (*env)->GetFieldID(env, clazz, "fd", "I"));
    CHECK_NULL(handle_fdID = (*env)->GetFieldID(env, clazz, "handle", "J"));
}

/**************************************************************
 * IOUtil.c
 */
JNIEXPORT jboolean JNICALL
Java_sun_nio_ch_IOUtil_randomBytes(JNIEnv *env, jclass clazz,
                                  jbyteArray randArray)
{
    return
        Java_sun_security_provider_NativeSeedGenerator_nativeGenerateSeed(env,
                                                                    clazz,
                                                                    randArray);
}

JNIEXPORT jint JNICALL
Java_sun_nio_ch_IOUtil_iovMax(JNIEnv *env, jclass this)
{
    return 16;
}

JNIEXPORT jlong JNICALL
Java_sun_nio_ch_IOUtil_writevMax(JNIEnv *env, jclass this)
{
    return java_lang_Long_MAX_VALUE;
}

jint
convertReturnVal(JNIEnv *env, jint n, jboolean reading)
{
    if (n > 0) /* Number of bytes written */
        return n;
    if (n == 0) {
        if (reading) {
            return IOS_EOF; /* EOF is -1 in javaland */
        } else {
            return 0;
        }
    }
    JNU_ThrowIOExceptionWithLastError(env, "Read/write failed");
    return IOS_THROWN;
}

jlong
convertLongReturnVal(JNIEnv *env, jlong n, jboolean reading)
{
    if (n > 0) /* Number of bytes written */
        return n;
    if (n == 0) {
        if (reading) {
            return IOS_EOF; /* EOF is -1 in javaland */
        } else {
            return 0;
        }
    }
    JNU_ThrowIOExceptionWithLastError(env, "Read/write failed");
    return IOS_THROWN;
}

JNIEXPORT jint JNICALL
Java_sun_nio_ch_IOUtil_fdVal(JNIEnv *env, jclass clazz, jobject fdo)
{
    return fdval(env, fdo);
}

JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_setfdVal(JNIEnv *env, jclass clazz, jobject fdo, jint val)
{
    setfdval(env, fdo, val);
}


#define SET_BLOCKING 0
#define SET_NONBLOCKING 1

JNIEXPORT void JNICALL
Java_sun_nio_ch_IOUtil_configureBlocking(JNIEnv *env, jclass clazz,
                                        jobject fdo, jboolean blocking)
{
    u_long argp;
    int result = 0;
    jint fd = fdval(env, fdo);

    if (blocking == JNI_FALSE) {
        argp = SET_NONBLOCKING;
    } else {
        argp = SET_BLOCKING;
        /* Blocking fd cannot be registered with EventSelect */
        WSAEventSelect(fd, NULL, 0);
    }
    result = ioctlsocket(fd, FIONBIO, &argp);
    if (result == SOCKET_ERROR) {
        NET_ThrowNew(env, WSAGetLastError(), "ioctlsocket");
    }
}

JNIEXPORT jboolean JNICALL
Java_sun_nio_ch_IOUtil_drain(JNIEnv *env, jclass cl, jint fd)
{
    char buf[16];
    jboolean readBytes = JNI_FALSE;
    for (;;) {
        int n = recv((SOCKET) fd, buf, sizeof(buf), 0);
        if (n == SOCKET_ERROR) {
            if (WSAGetLastError() != WSAEWOULDBLOCK) {
                JNU_ThrowIOExceptionWithLastError(env, "recv failed");
            }
            return readBytes;
        }
        if (n <= 0)
            return readBytes;
        if (n < (int)sizeof(buf))
            return JNI_TRUE;
        readBytes = JNI_TRUE;
    }
}

JNIEXPORT jint JNICALL
Java_sun_nio_ch_IOUtil_write1(JNIEnv *env, jclass cl, jint fd, jbyte b)
{
    int n = send((SOCKET) fd, &b, 1, 0);
    if (n == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) {
        JNU_ThrowIOExceptionWithLastError(env, "send failed");
        return IOS_THROWN;
    }
    return (n == 1) ? 1 : 0;
}

/* Note: This function returns the int fd value from file descriptor.
   It is mostly used for sockets which should use the int fd value.
*/
jint
fdval(JNIEnv *env, jobject fdo)
{
    return (*env)->GetIntField(env, fdo, fd_fdID);
}

void
setfdval(JNIEnv *env, jobject fdo, jint val)
{
    (*env)->SetIntField(env, fdo, fd_fdID, val);
}

jlong
handleval(JNIEnv *env, jobject fdo)
{
    return (*env)->GetLongField(env, fdo, handle_fdID);
}

 

해당 코드에서 중요한 부분은 다음이다.

 

result = ioctlsocket(fd, FIONBIO, &argp);

 

  • ioctlsocket: 윈도우 운영체제(OS)에게 "소켓의 설정을 바꿔줘!" 라고 명령하는 시스템 함수
  • FIONBIO: 이게 제일 중요합니다. "File I/O Non-Blocking I/O"의 약자입니다. 즉, "논블로킹 모드로 바꿔!"라는 명령 코드
  • &argp: 스위치. 위에서 blocking == JNI_FALSE면 SET_NONBLOCKING (즉, 1)이 들어감

 

결국에는 C언어 코드를 통해서 OS레벨에서 블로킹/논블로킹 사용을 설정해주지만, java에서는 C의 기능을 활용해서 이를 사용할 수 있게 하는 것이다.

 

2. Socket 연결하기

 

 

 

 

 

서버를 실행하고 cmd창에서 curl -v telnet://localhost:8080으로 찔러봣다.

 

그러니 curl이 해당 cmd창으로 응답을 보낸다.

 

그리고 서버에서는 다음과 같이 메시지가 나온다.

NIO Server Started on port 8080...
--- 현재 관리 중인 전체 채널 (1개) ---
 - [서버 소켓]: 8080 포트 감시 중
--------------------------------------------------
Client connected: /0:0:0:0:0:0:0:1:51391
--- 현재 관리 중인 전체 채널 (2개) ---
 - [클라이언트]: /0:0:0:0:0:0:0:1:51391
 - [서버 소켓]: 8080 포트 감시 중
--------------------------------------------------
Client connected: /0:0:0:0:0:0:0:1:51392
--- 현재 관리 중인 전체 채널 (3개) ---
 - [클라이언트]: /0:0:0:0:0:0:0:1:51392
 - [클라이언트]: /0:0:0:0:0:0:0:1:51391
 - [서버 소켓]: 8080 포트 감시 중
--------------------------------------------------
Client connected: /0:0:0:0:0:0:0:1:51393

 

 

자 그럼 이게 처음에 만든 그림과 맞는가?

 

 

selector는 현재 하나이다. 싱글톤 패턴으로 맨 처음에 생성하고, channel생성할 때 계속 동일한 selector를 사용한다.

 

그리고 새로운 요청이 올 때 마다 handleAccept 메소드로 새로운 channel을 생성해서 등록한다. 

 

private static void handleAccept(ServerSocketChannel serverSocket, Selector selector) throws IOException {
    SocketChannel client = serverSocket.accept();
    System.out.println("Client connected: " + client.getRemoteAddress());

    // ★ 중요: 클라이언트 소켓도 반드시 논블로킹으로 설정해야 함
    client.configureBlocking(false);

    // 클라이언트 소켓을 Selector에 등록 (이제부터 이 클라이언트가 보내는 데이터(READ)를 감시)
    client.register(selector, SelectionKey.OP_READ);
}

 

handleAccept의 맨 위에 코드를 보면 SocketChannel을 생성해서 여기서 register하는 부분이 있다. 

 

새로운 channel을 생성해서 key에 맞게 등록을 하는 것이다.

 

 

이에 대한 이해를 돕기 위해 이전 글을 다시 가져오겠다. (참고하려면 여기 클릭!)

 

부록 1에서 channel이 Netty에서 계속 생성이 되는건가 싶어서 질문을 했다.

 

Netty에서는 BossGroup과 WorkGroup으로 나누어서 channel들을 관리한다.

 

GossGroup에서는 bind()시에 부모 채널을 하나 만들어 놓고 접속이 들어올 때 마다 대화용 채널(자식) N개 생성해서 Work Group으로 날아가서 실제 일을 하게 된다.

 

이를 Netty의 멀티 리액터(Multi-Reactor) 패턴이라고한다.

 

나: BossGroup이랑 WorkGroup이 있고 여기에 일꾼들을 내가 원하는 만큼 등록할 수 있는 거지?
너: 맞다. 서버를 시작할 때 부트스트랩을 통해서 일꾼 역할을 하는 스레드들을 미리 생성해두는 개념이다. 따로 숫자를 지정하지 않으면 보통 현재 CPU 코어 수의 2배만큼 일꾼들이 준비된다.

나: 그리고 요청이 들어올 때마다 Channel이 계속 생성되는 구조라는 거지?
너: 그렇다. 새로운 접속 요청이 들어올 때마다 그 손님과 대화할 전담 통로인 Channel 객체가 하나씩 생성된다. 이 Channel은 힙 메모리에 생기는 아주 가벼운 자바 객체일 뿐이라서 수만 개가 동시에 생성되어도 서버가 잘 버티는 것이다.

나: 그러면 그 Channel들이 계속 늘어나도 문제가 없는 거야?
너: 이론적으로는 메모리가 허용하는 한 무한히 생성될 수 있다. 하지만 일하는 사람인 스레드 숫자는 고정되어 있기 때문에 수만 개의 채널이 생겨도 실제 CPU가 일하는 방식은 아주 효율적으로 유지된다. 이게 바로 적은 인원으로 수많은 손님을 상대하는 네티의 핵

 

아니 이 이야가기 왜 나오냐는 말인가?

 

selector는 현재 하나이다. 싱글톤 패턴으로 맨 처음에 생성하고, channel생성할 때 계속 동일한 selector를 사용한다.

 

라고 했는데, 뭔가 Netty도 별반 다르지 않은 구조라는 것을 확인할 수 있다.

 

어짜피 NIO에서 파생된 프레임워크이기 때문이다.

 

 

다음과 같이 AbstractSelectableChannel 클래스에 register 메소드가 있다.

 

여기서 키를 만들어서 Selector에 key에 딸린 channel을 함께 등록한다.

 

 

 

그렇게 되면 특정 요청에 대한 key를 selector가 기억하고 있으니, 이 key에 따른 channel을 그대로 실행시키면 된다.

 

 

그런데 현재 단일 스레드에서 작업하고 있다고? 그래서 thread 를 더 나눠서 크게 작업을 해보고 싶다고?

 

그것을 작업하기에는 동시성 문제, 블로킹 문제를 직접 해결해야 하는데 Netty를 사용하는 순간 이러한 어려운 고민은 할 필요가 없어진다. 

 


 

사실 이런거는 내 상황에서 굳이 몰라도 될 것이다.

 

하지만 엔지니어라는 입장에서 보았을 때, 어찌 이게 되는것인지 이해를 하고 나면 그 탐구 과정에서 

 

실력이 늘지 않을까 생각한다. 무엇보다도 너무 궁금했다. 코드를 어떻게 짯길래... 

 

국비 나부랭이 출신이지만, 엔지니어로서 살고자 하노라!