Spring/Framework 탐방 zone

[Netty] Netty가 뭐에요? - 부록1: - Netty의 전체 구성도, Channel 분석 및 Bootstrap 활용법

공대키메라 2026. 1. 25. 17:42

이전 글에서는 Protocol Support 를 알아보았다. (이전 글 클릭)

 

 

현재 이전 글들을 보면서 다시 점검하고 있는데,

 

Netty 시리즈 1~3탄 까지는 어느정도 써먹을 만 한데 그 이후는 영... 크게 머리에 쏙쏙 박히지 않고 

 

글을 작성한 본인조차 다시 보려니 뭘 말하고 싶은건지 명확히 와닿지 않는 부분이 많았다.

 

 

이러한 문제들은 차차 정리할 예정이지만 읽다보니 그런데 문득 생각이 들었다.

 

Netty에서는 다양한 Channel과 개발을 편리하게 해주는 기능을 제공해주고 있는데 Channel의 구현체들의 존재 이유와 

 

어느 상황에서 어느 Channel이 좋은건지 파악하고, 

 

Bootstrap의 명확한 사용방법을 큰 그림을 그려가면서 알아가려고 한다.

 

 

사실 Channel 에 대해서는 해당 글 중간에 알아 보았지만 그 부분을 현재 글로 마이그레이션했다 (여기 클릭!)

 

다시보는 나도 흐름이 좀 쌩뚱맞기도 하고 위에서 언급한 궁금한 부분을 정리하기에는 좀 애매해서 별도의 글로 작성하려고 한다.

 

 

아 그리고... 앞으로 글을 정리하면서 이번 글의 목표를 정의하고 결론에는 그 목표가 해소되었는지도 체크할 것이다.

 

 


목표

0. BootStrap에 대해 이해한다.

1. Netty의 전체 구성도를 이해한다.

2. Channel에 대해 이해한다.

3. Netty의 전체 구상도중 Channel에 어디에 속하는지 이해한다.

 


 

0. BootStrap 이해하기

Bootstrap은 우리가 작업하기 힘든 것을 상당부분 편리하게 할 수 있도록 도와준다는 뜻을 품고 있다.

 

Netty의 Bootstrap은 소켓 생성, 쓰레드 할당, 핸들러 등록 같은 저수준(Low-level)의 복잡한 초기화 과정을 개발자가 일일이 구현하지 않도록 자동화해 주는 도우미다.

 

Bootstrap을 사용하면 개발자가 직접 Java NIO Selector를 열고, 각 Channel이 어떤 Selector에 등록되어 있는지 추적하며, 특정 Thread가 해당 Channel의 이벤트를 처리하도록 수동으로 연결해 주는 번거로운 작업들이 모두 자동화된다.

 

생각을 해보면 Java NIO 구조에서도 Thread 하나당 하나의 selector를 가지고 있고,  이 selector하나가 여러 Channel을 관리하고 있다.

 

0.1) io.netty.bootstrap.AbstractBootstrap.java

다음 글은 해당 javadoc에서 확인할 수 있다.(여기 클릭!)

AbstractBootstrap은 채널(Channel)의 부트스트랩(초기 설정 및 실행 준비)을 쉽게 할 수 있도록 도와주는 헬퍼(Helper) 클래스입니다.

이 클래스는 메서드 체이닝(Method-chaining)을 지원하여, AbstractBootstrap을 간편하게 설정할 수 있는 방법을 제공합니다.

ServerBootstrap 문맥(서버 용도)으로 사용되지 않을 때, 이 bind() 메서드들은 데이터그램(UDP)과 같은 비연결성 전송 프로토콜에서 유용하게 사용됩니다.

 

여기서 "ServerBootstrap 문맥으로 사용되지 않을 때" 라는 말이 굉장히 헷갈린다.

 

이 말은, "TCP상황이 아닐때" 라는 것과 같다.

 

0.2) io.netty.bootstrap.Bootstrap.java

다음 글은 해당 javadoc에서 확인할 수 있다.(여기 클릭!)

 

Bootstrap은 위의 AbstractBootstrap을 상속한다.

클라이언트에서 사용할 채널을 쉽게 부트스트랩할 수 있도록 도와주는 부트스트랩입니다.

bind() 메서드는 UDP와 같은 비연결 전송 방식과 함께 사용하면 유용합니다.

일반 TCP 연결의 경우 제공된 connect() 메서드를 사용하십시오.

 

 

Netty에서는 개발자가 어렵게 Java NIO 구조를 직접 하는게 아니라, BootStrap 구현체를 이용해서 client 와의 통신을 쉽게 한다.

 

쓰레드를 직접 선택해서 그 쓰레드에 이벤트를 등록하고 처리하는 일련의 과정을 추상화해주기에 개발이 편해진다.

 

0.3) io.netty.bootstrap.ServerBootstrap.java

다음 을은 해당 javadoc에서 확인할 수 있다. (여기 클릭!)

ServerChannel(서버 채널)을 쉽게 초기화하고 실행 준비(Bootstrap)를 마칠 수 있도록 도와주는 Bootstrap의 하위 클래스입니다.

 

ServerChannel? 여기서 특정 Channel을 위한 Bootstrap을 왜 만들엇는지, 추후에 ServerChannel을 보고 파악하려고 한다.

 

0.4) Bootstrap 사용법

요약하자면, Netty에서 Bootstrap은 복잡한 네트워크 애플리케이션의 설정을 한곳에 모아 조립하고 실행해 주는 도우미 클래스다.

역할
쓰레드 그룹(EventLoopGroup), 사용할 소켓의 종류(Channel), 데이터 처리 로직(Pipeline)을 하나로 묶어 실제 네트워크 동작을 시작하게 만든다.


종류
ServerBootstrap: 외부의 접속을 기다리는 서버용
Bootstrap: 서버에 접속하는 클라이언트용

 

Netty의 Bootstrap 설정을 보면 .channel()과 .childHandler()를 각각 별도의 메서드로 호출하는 것을 볼 수 있습니다.

 

쉽게 말해 어떻게 연결할 것인가라는 전송(Transport)의 영역과 받은 데이터를 무엇을 할 것인가라는 로직(Logic)의 영역을 철저히 나누어 관리한다.

 

결국 Bootstrap이 이 두 가지를 따로 등록받는 이유는 유연성과 확장성 때문이다.

 

엔진과 로직을 분리함으로써 변화에 민감한 네트워크 하부 기술에 비즈니스 코드가 휘둘리지 않도록 설계된 것이다.

 

이러한 설계를 통해 개발자는 네트워크의 복잡함보다는 서비스 본연의 로직 개발에만 집중할 수 있게 된다.


ServerBootstrap 사용법 예시 코드

public void startServer() {
    EventLoopGroup bossGroup = new NioEventLoopGroup(1);   // 연결 수락 전담
    EventLoopGroup workerGroup = new NioEventLoopGroup(); // I/O 처리 전담

    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         // 1. 채널 종류 설정 (Transport 엔진 결정)
         .channel(NioServerSocketChannel.class) 
         // 2. 핸들러 설정 (비즈니스 로직 배관공사)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) {
                 ch.pipeline().addLast(new DiscardServerHandler()); 
             }
         });

        // 설정된 대로 포트 바인딩 및 서버 시작
        ChannelFuture f = b.bind(8080).sync();
        f.channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

 

 

흐름을 대강 정리하면 다음과 같다.

 

  • 쓰레드 그룹인 EventLoopGroup을 생성한다. 외부의 연결 요청을 감시하는 Boss 그룹과 실제 데이터 입출력을 전담하는 Worker 그룹을 각각 준비하는 과정이다.
  • 서버 설정을 돕는 ServerBootstrap을 생성하고 조립한다. 위에서 만든 쓰레드 그룹을 세팅하고 어떤 OS 채널 엔진을 사용할지 명시한다. 또한 연결된 소켓에서 실행될 비즈니스 로직인 핸들러를 파이프라인에 등록한다.
  • 특정 포트에 바인딩하여 서버를 가동한다. bind 메서드를 호출하여 사용할 포트 번호를 지정하면 이때부터 서버는 클라이언트의 접속을 받을 수 있는 활성 상태가 된다.
  • 서버가 닫힐 때까지 대기하고 자원을 정리한다. closeFuture를 통해 서버 채널이 닫히는 이벤트를 기다리며 작업이 모두 끝나면 shutdownGracefully를 호출해 사용했던 쓰레드 그룹과 메모리 자원을 안전하게 반납한다

 

0.5) Builder Pattern

현재 AbstractBootstrap, Bootstrap 그리고 ServerBootstrap 클래스는 빌더 패턴을 직접 구현하는데, 

 

단순 DTO가 아닌 복잡한 인프라를 설정하고 검증하는 프레임 워크 영역에서는 직접 구현을 통해 얻을 수 있는 이점을 위해서

 

번거롭지만 직접 구현을 하였다. 

 

장점은 다음과 같다.

  1. 세밀한 검증 : 유효성 검사 로직으로 잘못된 설정을 검증 및 차단
  2. 자유로운 상속 : 부모와 자식 빌더 같 공통 설정을 이용
  3. 외부 의존성 제거 : 순수 자바만으로 동작해서 깔끔함
  4. 명확한 디버깅 : 코드 흐름이 투명함.

 

예시로 HamburgerShop.java 코드를 Gemini에게 작성해달라고 했다.

public class HamburgerShop {
    // 1. 햄버거 객체 (최종 결과물)
    public static class Hamburger {
        String bread;
        String patty;
        String sauce;

        void eat() {
            System.out.println(bread + " 위에 " + patty + "를 올리고 " + sauce + "를 뿌린 햄버거를 먹습니다!");
        }
    }

    // 2. 부트스트랩 (빌더) 클래스
    public static class HamburgerBootstrap {
        private String bread;
        private String patty;
        private String sauce;

        // 설정을 할 때마다 '나 자신(this)'을 돌려줘서 다음 주문을 이어가게 합니다.
        public HamburgerBootstrap bread(String bread) {
            this.bread = bread;
            return this;
        }

        public HamburgerBootstrap patty(String patty) {
            this.patty = patty;
            return this;
        }

        public HamburgerBootstrap sauce(String sauce) {
            this.sauce = sauce;
            return this;
        }

        // Netty의 bind()와 같은 역할: 모든 설정을 마치고 '진짜 햄버거'를 만드는 순간
        public Hamburger build() {
            Hamburger burger = new Hamburger();
            burger.bread = this.bread;
            burger.patty = this.patty;
            burger.sauce = this.sauce;
            return burger;
        }
    }

    public static void main(String[] args) {
        // [사용 예시] 이게 바로 Netty 스타일입니다.
        HamburgerBootstrap shop = new HamburgerBootstrap();

        Hamburger myBurger = shop.bread("참깨빵")      // 설정 1
                .patty("순쇠고기 패티") // 설정 2
                .sauce("특별한 소스")   // 설정 3
                .build();             // <--- 여기서 햄버거가 '짠'하고 나옴!

        myBurger.eat();
    }
}

 

이제 우리는 더이상 어렵게 코드를 관리할 필요 없이 Bootstrap 쓰면 된다는것을 이해했고,

어떤 구현체들이 제공되는지 알아보았다.

 

그러면 이보다는 더 큰 그림으로 Netty의 전체 구성을 이해해보도록 하자.


1. Netty 전체 구성 이해 

직접 그림. 참고는 https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367

 

해당 목차의 글은 https://www.alibabacloud.com/blog/essential-technologies-for-java-developers-io-and-netty_597367

를 참고해서 정리를 하고 있지만, 필자가 느끼기에 빈 부분이 있어서 이를 채워서 설명하려고 한다.

 

이 부분은 사실 코드 레벨에서 필자가 일일히 찾을수도 없거니와, 블로그를 참고햇는데 블로그 글이 자세하지 않아 필자가 이해를 못한것으로 Gemini를 전적으로 많이 사용해서 다듬고 계속 검증했다.

 

1.1) Netty 아키텍처: BossGroup과 WorkerGroup의 협업 과정

1.1.1) NioEventLoopGroup의 역할 분담

 

BossGroup과 WorkerGroup은 모두 Netty에서 제공하는 NioEventLoopGroup 클래스를 사용한다.

같은 클래스를 사용하지만, 설정에 따라 역할만 다를 뿐이다.

  • BossGroup: 클라이언트의 연결 요청(Accept)만을 전담해서 받는다.
  • WorkerGroup: 연결이 성립된 이후, 해당 소켓의 데이터 입출력(I/O)과 실제 비즈니스 로직 처리를 담당한다.

1.1.2) NioEventLoop의 내부 구조

NioEventLoopGroup은 여러 개의 NioEventLoop 생명 주기를 관리한다.

 

각 NioEventLoop는 내부에 NIO Selector, Task Queue, 그리고 전담 Thread를 포함하고 있다.

 

이 세 가지 요소가 유기적으로 돌아가며 이벤트를 처리한다.

 

3가지 단계를 더 들여다보자. 다만 NioEventLoop가 Boss Group, Worker Group으로 나뉘어 있는데 두 개로 나누어서 보도록 하겠다.

 

Boss Group

Step1: select

클라이언트로부터 연결 요청 이벤트(OP_ACCEPT)가 들어오길 기다리거나 감지한다.

 

Step2: processSelectedKeys

연결 요청이 감지가 되면 OS커널로 승인을 받아 새로운 클라이언트와 통신할 수 있는 NioSocketChannel을 생성한다.

 

Step3: runAllTasks

Boss 스레드의 TaskQueue에 쌓여있는 기타 비동기 작업들을 순차적으로 실행

 

Worker Group

step 1: select

worker의 selector는 등록된 클라이언트 채널들 중, 데이터를 읽을 준비가 되었거나 쓸 준비가 된 채널이 있는지 감시한다.

(OP_READ / OP_WRITE)

 

step 2: processSelectedKeys

데이터가 들어온 채널을 찾아내어 버퍼로 데이터를 읽어드린다.

그 후, pipeline으로 데이터를 밀어넣는다. 이때 custom한 ChannelHandler들이 작동한다. 

 

step 3: runAllTasks

Netty가 멀티 스레드 환경에서도 lock없이 안전하게 동작하게 한다. 어떻게?

Netty에서는 애초에 한정된 스레드가 특정 channel을 담당해서 작동한다.

영원히 특정 채널은 특정 thread에서만 실행되기에 데이터가 꼬일 일도 없고, lock을 걸을 필요가 없다.

하지만 외부의 개입이 있는 경우에는, taskQueue에 그 일을 넣어놓는다. 그러면 순서대로 실행이 되서 꼬일 일이 없다.

 

1.1.3) 연결 요청과 Channel 생성

서버가 기동된 후, Boss Group에 속한 NioEventLoop의 Selector가 클라이언트의 접속 요청을 감지한다.

 

다시 말해서 Boss Group에서 관리하는 서버 소켓 NioServerSocketChannel이 클라이언트의 요청을 받으면 ServerSocketChannel.accept()를 호출하여 연결을 수락한다.

 

연결이 수락되는 즉시 Netty는 해당 클라이언트와의 통신을 전담할 새로운 NioSocketChannel 인스턴스를 생성한다.

즉, 새로운 클라이언트가 연결될 때마다 이 채널 객체는 계속해서 새롭게 생성된다.

 

주의할 점은, 이때 생성된 채널은 Boss Group과 클라이언트 간의 데이터 통로가 아니라는 것이다.

Boss Group은 단지 채널을 '생성'만 할 뿐, 실질적인 I/O 처리를 위해 이 채널의 소유권을 즉시 Worker Group의 EventLoop에 채널 등록(Register)을 위임한다.

 

1.1.4) ServerBootstrapAcceptor의 등장

Boss Group의 역할은 클라이언트의 연결을 수락하여 새로운 NioSocketChannel을 생성하는 것이다.

 

이 채널이 생성되는 즉시, Netty 서버 기동 시 파이프라인에 미리 등록되어 있던 ServerBootstrapAcceptor라는 특수 핸들러가 가동된다.

 

이 핸들러는 Boss 스레드와 Worker 스레드 사이를 연결하는 '브릿지' 역할을 수행한다.

 

Boss가 생성한 채널을 중간에서 가로챈 뒤, Worker Group 내에서 대기하고 있던 여러 NioEventLoop(작업자 스레드) 중 하나를 선택하여 해당 채널의 관리 및 I/O 처리를 비동기적으로 위임한다.

 

1.1.5) Worker 배정과 Selector 등록

ServerBootstrapAcceptor는 가로챈 채널을 WorkerGroup정해진 규칙(일반적으로 라운드 로빈, Round-Robin)에 따라 순차적으로 다음 NioEventLoop에게 전달한다.

  • 선택된 Worker의 NioEventLoop는 자신의 Selector에 이 NioSocketChannel을 등록한다.
  • 등록이 완료되는 순간부터, 해당 소켓에서 발생하는 모든 읽기/쓰기 데이터는 지정된 Worker 쓰레드가 전담해서 처리하게 된다.

 

이래도 이해가 잘 안돼서 Gemini와 문답을 나누었다. 궁금하면 열어서 보길 추천합니다!

 

더보기
[Netty 채널 생성 및 관리 원리에 대한 문답 정리]

Q:
클라이언트가 A, B, C... Z까지 있다고 치자. 서버를 띄울 때 처음 Boss Group에서 부모 채널(NioServerSocketChannel)이 만들어지는 건 맞지? 그리고 각각 A, B, C가 접속해서 작업을 요청하면, Worker Group에 채널이 3개 생성되는 거야? 그다음은 어떻게 돼?

A:
맞습니다. 흐름을 단계별로 보면 이렇습니다.

서버 기동 시: Boss Group에 부모 채널 1개가 생성되어 특정 포트(예: 8080)를 잡고 대기합니다.
A, B, C 접속 시: 부모 채널이 접속 신호를 감지할 때마다 Boss 스레드가 accept()를 호출해 자식 채널(NioSocketChannel)을 하나씩 총 3개 만듭니다. 이들은 즉시 Worker Group의 일꾼(EventLoop)들에게 배정됩니다.

그다음 상황: 만약 새로운 클라이언트 D, E...가 계속 들어오면 자식 채널은 계속 늘어납니다. 하지만 이미 접속한 A가 두 번째, 세 번째 작업을 요청할 때는 채널이 새로 생성되지 않고 기존 채널을 재사용합니다.

Q:
그러니까 작업하는 도중에 요청이 또 들어오면, 새로운 채널을 만들지 않고 이미 만들어진 같은 채널로 요청을 보낸다는 거지?

A:
정확합니다! 그게 바로 Netty가 고성능을 내는 비결입니다.

연결 시 생성: 자식 채널은 '데이터 요청'마다 생기는 게 아니라, 처음 '물리적 연결(TCP Connection)'이 맺어질 때 딱 한 번 생성됩니다.

작업 시 재사용: 이미 연결된 상태에서 클라이언트가 보내는 수많은 작업(Request)은 이미 Worker Group에 배정된 그 동일한 채널(통로)을 그대로 타고 흐릅니다.
연결 종료 시 소멸: 사용자가 접속을 완전히 끊을 때만 해당 채널 객체가 폐기되어 메모리(GC)에서 사라집니다.

Q:
정리하자면, 요청마다 채널이 생성되긴 하는데 다만 그것을 Worker Group이 관리하는 구조라고 이해하면 될까?

A:
용어를 조금 더 엄밀하게 다듬으면 완벽합니다.
'요청(Data Request)'마다 생성되는 것이 아니라, '접속(Connection)'마다 생성되는 것입니다.

접속(Connection): Boss Group이 자식 채널을 new로 생성해서 Worker Group에 던져주는 시점입니다.
요청(Request): 이미 생성되어 Worker Group이 관리 중인 채널을 통해 데이터만 주고받는 행위입니다.새로운 객체 생성이 없습니다.

결론 요약:"Boss Group은 부모 채널 1개로 문을 지키고 있다가, 새로운 손님이 접속할 때마다 전담 통로인 자식 채널을 하나씩 만들어 Worker Group에게 넘긴다. Worker Group은 넘겨받은 채널을 꽉 잡고 있으면서, 손님이 나갈 때까지 수많은 작업 요청을 그 채널 하나로 계속 처리(재사용)한다.

 

2. Channel 이해하기

Channel은 Netty에서 제공하는 Interface이다.

 

io.netty.channe.Channel (여기 클릭!)

채널은 네트워크 소켓(Socket)에 대한 연결점, 또는 읽기(read), 쓰기(write), 연결(connect), 바인딩(bind)과 같은 I/O 작업을 수행할 수 있는 구성 요소를 의미합니다.

채널은 사용자에게 다음과 같은 기능을 제공합니다

채널의 현재 상태: 채널이 열려 있는지(open), 또는 연결되어 있는지(connected) 등의 상태 정보를 확인할 수 있습니다.

채널 설정 파라미터: 수신 버퍼 크기(receive buffer size)와 같은 채널의 구성 설정을 확인하거나 변경할 수 있습니다.

지원하는 I/O 작업: 해당 채널이 지원하는 I/O 작업(읽기, 쓰기, 연결, 바인딩 등)을 수행할 수 있습다.

ChannelPipeline(채널 파이프라인): 채널과 관련된 모든 I/O 이벤트 및 요청을 처리하는 '채널 파이프라인'을 제공합니다.

핵심 특징
모든 I/O 작업은 비동기(Asynchronous) 방식으로 이루어집니다. 즉, 어떤 작업(예: 데이터 전송)을 요청하면 그 작업이 완전히 끝날 때까지 기다리지 않고 즉시 다음 작업을 수행할 수 있습니다

... 

 

 

사실 이걸 일일히 들여다보기는 싫기에, 정리를 요청했다.

 

Channel의 주요 메서드 정리!

Netty의 Channel 인터페이스는 다양한 상태 관리 메서드를 제공한다:

상태 조회 메서드
1. isOpen(): 채널이 열려 있는가?
2. isRegistered(): EventLoop에 등록되었는가?
3. isActive(): 활성 상태이고 연결되어 있는가?
4. isWritable(): 쓰기가 가능한가?

설정 및 정보
1. config(): 채널 설정 반환
2. pipeline(): ChannelPipeline 반환
3. eventLoop(): 연결된 EventLoop 반환

I/O 작업
1. read(), write(), flush()
2. connect(), bind(), close()

기존 Java NIO의 Channel은 isOpen()과 close() 정도만 제공했지만, Netty는 훨씬 풍부한 상태 관리와 제어 기능을 제공한다.

 

Channel은 단순한 채널, 즉 연결점이 아니라 내부적으로 여러 일을 해주는것 같다. 

 

말 그대로 채널을 생성한다고 하면 우리가 작업할 수 있는 여러 창구를 개설한다고 이해했따.

 

Channel관련해서 이것과 연관된 것들을 보려고 한다. 약간 노잼이겠지만... 

 

2.1) Channel Handler

ChannelInboundInvoker와 ChannelOutboundInvoker 작업을 다루거나 intercept하고 ChannelPipeline에서 다음 handler로 넘긴다.

 

인바운드와 아웃바운드는 트래픽에 네트워크 간에 이동하는 방향을 말하는데,

 

인바운드라고 하면 네트워크 안으로 들어오는 것이고, 아웃바운드는 네트워크에서 나가는 것이다.

 

ChannelHandler는 ChannelHandlerContext 객체에서 제공받는다.

 

ChannelHandler는 컨텍스트 객체를 통해 자신이 속한 ChannelPipeline과 상호작용하도록 되어 있다.

 

컨텍스트 객체를 사용하여, ChannelHandler는 이벤트를 상위(upstream) 또는 하위(downstream)로 전달하거나, 파이프라인을 동적으로 수정하거나, 핸들러에 특화된 정보를 저장(AttributeKey 사용)할 수 있다.

 

2.2) ChannelHandlerContext

ChannelHandler가 ChannelPipeline과 다른 handler들과 상호작용 할 수 있게 한다. 

Handler는 다음 ChannelHandler와 ChannelPipeline으로 알리거나 동적으로 속하는 ChannelPipeline을 수정할 수 있다.

 

2.3) ChannelHandlerAdapter

ChannelHandler를 위한 스켈레톤 구현체.

 

해골 구현체...? 뒤에 다시 알아보자.

 

2.4) ChannelInboundHandler

ChannelHandler는 상태 변경에 대한 콜백을 추가하는 핸들러입니다.
이를 통해 사용자는 상태 변경에 쉽게 hook을 걸 수 있습니다.

 

Hook? 이거는 왜 여기서 나오는 것인가... Hook이라는게 Java에도 있던건지 나는 잘 몰랐다.

 

react 에 대해서 공부를 좀 했었는데, 이에 대해서 좀 알아봐야겠다.

 

2.5) ChannelInboundHandlerAdapter

ChannelInboundHandler 구현체들을 위한 추상 베이스 클래스로, 모든 메서드의 구현을 제공합니다.

이 구현은 단순히 작업을 ChannelPipeline의 다음 ChannelHandler로 전달합니다.

서브클래스는 이를 변경하기 위해 메서드 구현을 오버라이드할 수 있습니다.

 

 

여기서 쭉 나온 의문점을 정리해보면

 

ChannelPipeline, Hook, 해골 구현체? 그리고 구조에 대한 이해가 잘 되지 않았다.

 

위 4가지에 대해서 좀 알아보도록 하겠다.

 

2.6) ChannelPipeline and structure, Hook, Skeleton implementation

필자가 ChannelPipeline에 대한 글을 찾던 중, 아주 잘 정리한 Medium의 글을 알게 되었다. (여기 클릭)

 

사실 공식 문서에서는 설명을 하고 예시를 주고 땡(?)이다. 

 

공식문서도 나름 잘 정리되어 있기는 하지만, 이를 더 잘 정리한 고수님들의 글을 참고하면 좋을 듯하다.

 

2.8) Netty의 데이터 모델 과 Channel 그리고 ChannelPipeline

데이터 모델

netty의 데이터모델은 꽤 직설적이다. 핵심 타입은 Channel인데, ChannelPipeline을 가지고 있고 EventLoopGroup에서 하나의 EventLoop와 연관되어 있다.

 

각 채널들은 자신만의 채널 파이프라인을 가지고 있고, 이는 Handler들을 넘기는 구조이다.

 

인터페이스 ChannelPipelines

한 채널의 inbound 이벤트들과 outbound 작업들을 다루거나 가로채는(intercept) ChannelHandler의 리스트.

ChannelPipeline은 Intercepting Filter 패턴의 고급 형태를 구현하여, 사용자에게 이벤트가 어떻게 처리되는지
그리고 파이프라인 내의 ChannelHandler들이 서로 어떻게 상호작용하는지에 대한 완전한 제어권을 제공한다;

 

채널 안에 채널 파이프라인이 있고, 이것을 EventLoopGroup 내부의 EventLoop가 바라보는 형식이다.

 

여기서 ChannelPipeline은 Intercepting Filter 패턴의 고급 형태를 구현한다는데,

 

단순한 filter에서 뭔가 ... 기능이 더 추가된걸로 이해된다.

 

Hook

상태변경 시점에 낚아채서 별도의 작업을 할 수 있도록 하는 기능이다. 

Handler이것들이 Hook, 즉 무언가 상태가 변경되는 시점인가보다.

 

 

여러 상태가 있는데, 기존의 nio 에서 지원하는 channel의 상태는 isOpen, close 밖에 없지만 netty에서의 channel은 훨씬 많은 상태 제어를 제공한다.

 

Skeleton Implementation

덜덜덜~ 해골 무셔~

 

 

찾아보니 Effective Java에서 소개된 패턴이라고한다.

 

[이펙티브 자바] 아이템 20 - 추상 클래스보다는 인터페이스를 우선하라 인데...

 

인터페이스와 추상클래스를 결합한 패턴이며, 인터페이스만 사용시 중복 코드가 계속 생기지만, 이러한 중복 코드를 추상클래스로 뺀 것이다.

 

즉, 미리 추상클래스에서 선언해놓고, 실제로 상속받아서 구현하는 것의 경우에는 원하는 것만 override하면 된다.

 

예시

// 1. 인터페이스 (규약)
public interface MyInterface {
    void method1();
    void method2();
    void method3();
    void method4();
}

// 2. 추상 골격 클래스 (공통 구현)
public abstract class AbstractMyInterface implements MyInterface {
    // 공통 구현 제공
    @Override
    public void method1() {
        // 기본 구현
    }
    
    @Override
    public void method2() {
        // 기본 구현
    }
    
    // method3, method4는 추상 메서드로 남겨둠
    // (구현 클래스가 반드시 구현해야 함)
}

// 3. 구체 클래스 (필요한 것만 구현)
public class ImplA extends AbstractMyInterface {
    @Override
    public void method3() {
        // A만의 구현
    }
    
    @Override
    public void method4() {
        // A만의 구현
    }
    // method1, method2는 상속받아서 사용!
}

 

 

4. Channel이 생성되는 순간은? 그림의 어디에 속할까?

위의 글을 잘 읽었으면 알 수 도 있지만, 사실 두 부분에서 channel이 생성된다.

 

4.1) 부모 채널(NioServerSocketChannel)의 생성 

시점: bind()를 호출하는 순간

  • 생성 위치:  Boss Group 박스가 가동되기 직전, 메인 쓰레드에서 생성된다.
  • 역할: 이 채널은 실제 데이터를 주고받지 않고, 오직 "손님이 왔나?"만 감시한다. 
  • 이후 흐름: 생성된 직후에 Boss Group 안에 있는 Selector에 등록(Register)되면  즉, 클라이언트 연결 요청 수락(Accept) 하게 되면 본격적으로 그림의 루프가 돌기 시작한다. 

해당 NioServerSocketChannel은 BossGroup (Parent Group)이 관리한다.

 

BossGroup의 스레드수는 보통 포트당 하나라고한다.

 

4.2) 자식 채널(NioSocketChannel)의 생성

시점: Boss Group의 step 2: processSelectedKeys 단계

  • Event 발생: 클라이언트가 접속하면 Boss의 Selector가 이를 감지 (step 1: select).
  • 진짜 생성! step 2: processSelectedKeys에서 Boss 쓰레드가 accept()를 호출. 이때 실제 통신을 담당할 새로운 NioSocketChannel 객체가 생성
  • 배달: 생성된 자식 채널은 네 그림에 화살표로 그려진 "Register the channel to the selector"를 타고 Worker Group으로 넘어감
  • BossGroup에서 넘겨받은 SocketChannel은 WorkGroup에서 관리한다.

 

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

 

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

 

 

뭐? 기존의 Tomcar같은 경우에는 요청 1개당 Thread한개가 생성되는건데 이걸 막기 위해서...

 

비동기로 Thread하나가 여러 작업을 할 수 있게 만든것 아닌가? 

 

그런데 Channel이 여러개가 만들어진다고? 

 

이에 대한 궁금증에 대해서 AI에게 질문을 했다. 궁금하면 다음을 클릭!

더보기
키메라 : 
그런데 channel이 너무 많이 많아져도 netty는 터지지 않자나.tomcat의 경우에 thread가 요청당 여러개가 생성이 되는데,이것이 channel이 여러개가 생성되는 거로 바뀌면...

Gemini : 
맞아, 처음 보면 "어차피 객체가 계속 생성되는데 뭐가 다른 거지?"라는 의문이 드는 게 당연해. 하지만 **'스레드(Thread)'**와 **'채널(Channel)'**은 체급 자체가 완전히 달라.
그 핵심적인 차이를 4가지 포인트로 짚어줄게.

1. 리소스의 무게: 스택(Stack) vs 힙(Heap)
가장 큰 차이는 메모리를 점유하는 방식이야.
스레드 (Tomcat 스타일): 자바에서 스레드 하나가 생성되면 기본적으로 약 1MB 정도의 스택(Stack) 메모리를 점유해. 만약 1만 개의 연결을 위해 1만 개의 스레드를 만들면, 단순 계산으로도 10GB의 메모리가 스택 영역에서만 사라지는 거지.채널 (Netty 스타일): 채널은 그냥 힙(Heap) 영역에 생성되는 가벼운 자바 객체일 뿐이야. 연결 하나당 수십~수백 바이트 정도의 상태 정보와 파일 디스크립터(FD) 정도만 가지고 있어서, 수만 개가 생겨도 메모리 압박이 훨씬 덜해.

2. 일하는 방식: 전담 마크 vs 멀티태스킹
전통적인 방식과 Netty의 결정적인 차이는 **'멀티플렉싱(Multiplexing)'**에 있어.
Tomcat (전통적): 손님(요청) 한 명당 웨이터(스레드) 한 명이 붙어서, 손님이 메뉴를 고르는 동안(I/O 대기) 웨이터도 아무것도 못 하고 같이 멍하니 서 있어야 해.Netty: **이벤트 루프(EventLoop)**라는 숙련된 웨이터 한 명이 **셀렉터(Selector)**라는 알림판을 들고 있어. 수천 명의 손님(채널) 중 "주문할게요!"(데이터 도착)라고 알림이 뜬 테이블만 잽싸게 가서 처리하고 바로 다음 테이블로 이동해. 그래서 적은 수의 스레드로도 수만 개의 채널을 관리할 수 있는 거야.

3. 컨텍스트 스위칭(Context Switching)의 비용
스레드가 많아지면 CPU는 일을 하는 시간보다 "누가 일할 차례인지" 결정하고 상태를 갈아끼우는 데 더 많은 에너지를 써.
스레드가 수천 개면 CPU는 컨텍스트 스위칭 오버헤드 때문에 비명을 지르며 성능이 떡락해.Netty는 CPU 코어 수에 맞춘 소수의 스레드만 돌리기 때문에 CPU가 딴짓 안 하고 실제 데이터 처리에만 집중할 수 있어.

4. 진짜 한계점(Bottleneck)이 달라
그래서 "터진다"는 기준이 두 프레임워크가 달라.
Tomcat: 보통 메모리(스택) 부족이나 컨텍스트 스위칭으로 인한 CPU 포화로 서버가 뻗어버려.Netty: 채널이 아무리 많아져도 서버는 잘 안 터져. 대신 OS의 파일 디스크립터(File Descriptor) 제한에 걸리거나, 버퍼를 너무 많이 써서 힙/다이렉트 메모리가 부족해지는 게 일반적인 한계점이야.

요약하자면: 스레드는 **'무거운 전담 인력'**이고, 채널은 **'가벼운 연결 통로'**야. Netty는 연결(채널)은 늘려도 일하는 사람(스레드)은 고정해두기 때문에 수만 건의 동시 접속도 거뜬히 버틸 수 있는 거지.
이제 왜 Netty가 대규모 채팅이나 게임 서버처럼 **'연결은 많은데 실제 데이터 전송은 띄엄띄엄 일어나는 상황'**에서 깡패인지 이해가 좀 되지?
실제로 Netty가 메모리를 아끼기 위해 사용하는 '바이트 버퍼 풀링(ByteBuf Pooling)' 기술에 대해서도 파볼까? 아니면 리눅스에서 파일 디스크립터 설정을 어떻게 늘려줘야 하는지 알려줄까?

 

 

5. 그 외 정리사항

 

이것은 필자가 AI에게 질문한 내용인데, 아직도 헷갈려서... 이것을 좀 적어놓는다.

 

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

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

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

 

 


 

이번 글의 목표를 되짚어보자.

 

0. BootStrap에 대해 이해한다.

1. Netty의 전체 구성도를 이해한다.

2. Channel에 대해 이해한다.

3. Netty의 전체 구상도중 Channel에 어디에 속하는지 이해한다.

 

어느정도 이해가 되었는가? (내 스스로에게 묻는 말...)

 

 

사실 엄청 low-level한것도 보고 싶었는데, 코드가 너무 방대하고 이 연관관계를 파악하고 있다가는 취지에 어긋나는것 같아서

 

너무 깊은 부분들은 과감히 생략했다.

 

무엇보다도 이것만 공부하리...? 어느정도 큰 틀을 잡고 세부적으로 시간이 날 때 또 정리할 예정이다.

 

과연 이번에 글을 적고 나서 다음에 봣을 때 이해가 될것인지, 몇일 혹은 몇주 뒤에 다시 보고 글이 바뀔지 판단하도록 하겠다.

 


 

2026-02-19

그럼 그렇지... 다시 정리를 했다. 

 

Netty 의 전체 구성 이해하기 부분은 아무래도 참고한 알리바바의 기술 블로그가 이해가 되지 않아서,

 

AI를 활용해서 문답하면서 얻어낸 답을 적었다.

 

또한, 오타도 전면 수정되었고 덕분에 다시 많이 배웠다!