Netty 서버 시작 과정의 핵심 소스 분석

  1. NioEventLoopGroup 초기화 및 이벤트 루프 생성 Netty에서 서버는 일반적으로 NioEventLoopGroup을 통해 비동기 이벤트 처리를 수행하며, 이 그룹은 두 가지 역할로 나뉜다: BossGroup와 WorkerGroup. 본문에서는 서버 리스닝을 담당하는 BossGroup에 초점을 맞춘다.

NioEventLoopGroup 생성자는 내부적으로 여러 스레드 기반의 이벤트 루프를 생성한다. 각각의 루프는 NioEventLoop 인스턴스로 구성되며, 이들은 작업 큐와 선택자(Selector)를 관리한다.

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                        EventExecutorChooserFactory chooserFactory, Object... args) {
    if (nThreads <= 0) {
        throw new IllegalArgumentException("Thread 수는 0 이상이어야 합니다.");
    }

    // 기본 실행자 설정
    if (executor == null) {
        executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
    }

    children = new EventExecutor[nThreads];
    for (int i = 0; i < nThreads; i++) {
        boolean success = false;
        try {
            children[i] = newChild(executor, args);
            success = true;
        } catch (Exception e) {
            throw new IllegalStateException("이벤트 루프 생성 실패", e);
        } finally {
            // 정리 로직 생략
        }
    }

    // 스레드 선택 전략 등록
    chooser = chooserFactory.newChooser(children);
}

여기서 newChild()는 각 스레드에 대응하는 NioEventLoop 인스턴스를 생성한다. 이 인스턴스는 select() 메서드를 반복 호출하며, 입출력 이벤트를 감지하고 처리한다.


  1. NioServerSocketChannel 생성 과정 ServerBootstrapchannel(NioServerSocketChannel.class)을 호출하면, 내부적으로 ReflectiveChannelFactory를 사용해 해당 채널 클래스를 반사(Reflection) 방식으로 인스턴스화한다.
public B channelFactory(ChannelFactory<? extends C> factory) {
    this.channelFactory = Objects.requireNonNull(factory, "factory");
    return (B) this;
}

이러한 팩토리 객체는 이후 채널 생성 시점에 사용된다.


  1. 바인딩 및 채널 등록 bind() 메서드 호출 시, 서버는 실제 포트를 할당하고 네트워크 리스닝을 시작한다.
private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();

    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // 비동기 처리 후속 처리
    }
}

initAndRegister()는 채널 생성과 등록을 동시에 수행한다.

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        channel = channelFactory.newChannel(); // 반사 생성
        init(channel); // 채널 초기화
    } catch (Throwable t) {
        if (channel != null) {
            channel.unsafe().closeForcibly();
        }
        return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
    }

    // 그룹에 등록 → BossGroup의 하나의 이벤트 루프 선택
    ChannelFuture regFuture = config().group().register(channel);

    if (regFuture.cause() != null) {
        channel.close();
    }

    return regFuture;
}

  1. 채널 초기화 및 파이프라인 설정 init(Channel) 메서드는 채널의 설정 옵션, 속성, 그리고 ChannelPipeline에 핸들러를 추가한다.
void init(Channel channel) throws Exception {
    // 옵션 설정
    synchronized (options) {
        channel.config().setOptions(options);
    }

    // 속성 설정
    synchronized (attrs) {
        for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
            channel.attr((AttributeKey<Object>) e.getKey()).set(e.getValue());
        }
    }

    // 파이프라인 구성
    ChannelPipeline p = channel.pipeline();
    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            // 상위 핸들러 추가
            if (config.handler() != null) {
                p.addLast(config.handler());
            }

            // 클라이언트 연결 수락기 등록
            ch.eventLoop().execute(() -> {
                p.addLast(new ServerBootstrapAcceptor(
                    childGroup, 
                    childHandler, 
                    childOptions.toArray(newOptionArray(childOptions.size())),
                    childAttrs.toArray(newAttrArray(childAttrs.size()))
                ));
            });
        }
    });
}

이 과정에서 중요한 점은, 클라이언트 연결을 수용하기 위한 ServerBootstrapAcceptor 가 동적으로 파이프라인에 추가된다는 점이다. 이 핸들러는 새로운 클라이언트 연결이 발생했을 때, WorkerGroup의 이벤트 루프에 새 SocketChannel을 등록하여 처리하도록 한다.


  1. 채널의 넌블로킹 모드 설정 생성된 ServerSocketChannel은 기본적으로 블로킹 모드이므로, 다음처럼 비동기 처리를 위해 설정된다.
private static ServerSocketChannel newSocket(SelectorProvider provider) {
    try {
        return provider.openServerSocketChannel();
    } catch (IOException e) {
        throw new ChannelException("서버 소켓 열기 실패", e);
    }
}

// AbstractNioChannel 생성자
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = readInterestOp;
    try {
        ch.configureBlocking(false); // 비동기 모드 활성화
    } catch (IOException e) {
        throw new ChannelException("비동기 모드 설정 실패", e);
    }
}

이로써 Selector는 이벤트 기반으로 다양한 클라이언트 연결을 효율적으로 처리할 수 있다.

태그: Netty NioEventLoopGroup ServerBootstrap ChannelPipeline NioServerSocketChannel

5월 29일 06:45에 게시됨