Java로 고성능 네트워크 애플리케이션을 만들겠다고 결심하면, NIO를 직접 다루는 길과 프레임워크를 쓰는 길 두 가지가 있다. 대부분은 프레임워크를 선택하는데, 그 선택지에는 항상 세 이름이 등장했다 — Apache Mina, Oracle Grizzly, 그리고 Netty. 이 시리즈의 마지막 글에서 이 셋을 비교하고, 왜 Netty가 사실상 표준이 되었는지 정리합니다.

Java NIO 프레임워크가 왜 필요했는가

Java NIO(java.nio)는 JDK 1.4에서 등장했지만, Selector와 ByteBuffer를 직접 다루는 건 고통스러운 일 이었습니다. 시리즈 1편에서 정리했듯이 몇 가지 구조적 문제가 있었습니다.

  • ByteBufferflip() 호출을 빠뜨리면 데이터가 깨진다
  • Selector의 epoll bug(리눅스에서 CPU 100% 점유)를 직접 우회해야 한다
  • 연결 상태 관리, 스레드 모델, 프로토콜 파싱을 전부 직접 구현해야 한다

이 고통을 해결하기 위해 2000년대 중반, 거의 동시에 세 프레임워크가 등장합니다.

프레임워크시작 시기주체핵심 용도
Apache Mina2004년Apache Software Foundation범용 NIO 프레임워크
Grizzly2005년Sun(→Oracle)GlassFish 서버 내장
Netty2004년 (JBoss 시절)Trustin Lee → JBOSS → 독립범용 비동기 네트워크 프레임워크

Apache Mina — Netty의 출발점

Mina의 역사

Apache Mina(Multipurpose Infrastructure for Network Applications)는 Java NIO 위에 이벤트 기반 네트워크 프레임워크를 구축한 초기 프로젝트입니다. Trustin Lee 가 핵심 개발자로 참여했다는 점이 중요합니다. Netty의 창시자가 바로 이 사람이거든요.

Trustin Lee는 Mina를 개발하면서 설계적 한계를 체감했고, 이를 근본적으로 개선한 새로운 프레임워크를 만들기로 합니다. 그게 Netty입니다.

Mina의 아키텍처

Mina의 구조는 세 가지 핵심 개념으로 이루어져 있습니다.

PLAINTEXT
┌─────────────────────────────────────────┐
│              IoService                  │
│  (IoAcceptor / IoConnector)             │
├─────────────────────────────────────────┤
│           IoFilterChain                 │
│  ┌──────┐  ┌──────┐  ┌──────┐          │
│  │Filter│→│Filter│→│Filter│          │
│  └──────┘  └──────┘  └──────┘          │
├─────────────────────────────────────────┤
│            IoHandler                    │
│  (비즈니스 로직)                         │
└─────────────────────────────────────────┘
  • IoService: 네트워크 I/O를 담당하는 최상위 인터페이스. 서버용 IoAcceptor와 클라이언트용 IoConnector로 나뉩니다
  • IoFilterChain: 데이터가 흐르는 필터 체인. 로깅, 코덱, SSL 같은 크로스커팅 로직을 처리합니다
  • IoHandler: 최종 비즈니스 로직. messageReceived(), sessionOpened() 같은 콜백을 구현합니다
JAVA
// Mina 서버 예시
IoAcceptor acceptor = new NioSocketAcceptor();

// 필터 체인 구성
acceptor.getFilterChain().addLast("codec",
    new ProtocolCodecFilter(new TextLineCodecFactory()));
acceptor.getFilterChain().addLast("logger",
    new LoggingFilter());

// 비즈니스 로직 핸들러
acceptor.setHandler(new IoHandlerAdapter() {
    @Override
    public void messageReceived(IoSession session, Object message) {
        String line = (String) message;
        session.write("에코: " + line);  // 받은 메시지를 에코
    }
});

acceptor.bind(new InetSocketAddress(8080));

Mina의 한계

Mina는 훌륭한 시작이었지만, 시간이 지나면서 몇 가지 설계적 문제가 드러났습니다.

  • **IoSession의 과도한 책임 **: 연결 상태, 속성 저장, 쓰기 등 너무 많은 역할을 한 객체에 담았습니다
  • **ByteBuffer 래퍼의 한계 **: IoBuffer는 java.nio.ByteBuffer를 래핑한 것이어서 근본적인 한계(flip 필요, 고정 크기 등)를 벗어나지 못했습니다
  • ** 비동기 쓰기의 불완전함 **: session.write()의 반환 타입이 WriteFuture이지만, 완료 콜백 체이닝이 불편했습니다
  • ** 스레드 모델의 경직성 **: I/O 스레드와 비즈니스 로직 스레드의 분리가 깔끔하지 않았습니다

Oracle Grizzly — GlassFish의 엔진

Grizzly의 탄생 배경

Grizzly는 Sun Microsystems가 GlassFish 애플리케이션 서버 의 HTTP 처리 엔진으로 개발한 NIO 프레임워크입니다. 처음에는 GlassFish 전용이었지만, 나중에 독립 프레임워크로도 사용할 수 있게 되었습니다.

Oracle이 Sun을 인수한 뒤에도 Grizzly는 유지되었지만, GlassFish가 Jakarta EE로 전환되면서 Grizzly의 입지도 점점 줄어들었습니다.

Grizzly의 특징

JAVA
// Grizzly HTTP 서버 예시
HttpServer server = HttpServer.createSimpleServer("/", 8080);

server.getServerConfiguration().addHttpHandler(
    new HttpHandler() {
        @Override
        public void service(Request request, Response response)
            throws Exception {
            response.setContentType("text/plain");
            response.getWriter().write("Hello from Grizzly");
        }
    }, "/hello");

server.start();

Grizzly의 독특한 점은 NIO.2(AIO, Asynchronous I/O) 지원 입니다.

PLAINTEXT
NIO vs AIO 비교

NIO (Non-blocking I/O):
  애플리케이션 → Selector에 등록 → 준비된 채널 확인 → 직접 읽기/쓰기
  (이벤트를 폴링하는 방식)

AIO (Asynchronous I/O, NIO.2):
  애플리케이션 → OS에 읽기 요청 + 콜백 등록 → OS가 완료 시 콜백 호출
  (OS가 I/O를 완료하고 알려주는 방식)

이론적으로 AIO가 더 효율적이어야 하지만, 리눅스에서 AIO는 내부적으로 스레드 풀 기반 시뮬레이션 이어서 실제 성능 이점이 거의 없습니다. Windows의 IOCP에서는 AIO가 잘 작동하지만, 서버 환경의 대부분이 리눅스인 현실에서 이 장점은 크게 빛을 발하지 못했습니다.

Grizzly의 현재

  • GlassFish/Payara가 Grizzly를 내부적으로 사용하고 있어서 완전히 죽은 건 아닙니다
  • 하지만 신규 프로젝트에서 Grizzly를 선택하는 경우는 거의 없습니다
  • Jersey(JAX-RS 참조 구현)가 Grizzly를 테스트 컨테이너로 사용하는 것 정도가 현재 주요 용도입니다

Netty의 승리 요인

세 프레임워크가 같은 문제를 풀려고 했는데, 왜 Netty만 살아남았을까요? 단순히 "성능이 좋아서"가 아닙니다. 핵심은 API 설계 철학과 커뮤니티 에 있습니다.

1. 일관된 API 설계

Netty의 API는 처음부터 끝까지 일관된 패턴을 따릅니다.

JAVA
// Netty — 서버든 클라이언트든 같은 패턴
Bootstrap b = new Bootstrap();  // 또는 ServerBootstrap
b.group(group)
 .channel(NioSocketChannel.class)      // 또는 NioServerSocketChannel
 .handler(new ChannelInitializer<>() {
     @Override
     protected void initChannel(Channel ch) {
         ch.pipeline().addLast(new MyHandler());
     }
 });
  • 서버와 클라이언트의 부트스트래핑이 같은 구조
  • ChannelChannelPipelineChannelHandler라는 명확한 계층
  • 모든 I/O 작업이 ChannelFuture를 반환하는 비동기 패턴

Mina는 IoAcceptor/IoConnector, IoSession, IoHandler, IoFilter로 개념이 분산되어 있고, 역할 경계가 모호한 부분이 있었습니다.

2. ByteBuf — 근본적으로 다른 버퍼

이건 이 시리즈에서 여러 번 강조했던 부분입니다.

특성java.nio.ByteBuffer / Mina IoBufferNetty ByteBuf
읽기/쓰기 인덱스하나의 position (flip 필요)readerIndex / writerIndex 분리
크기 조절불가 (새 버퍼 할당 필요)동적 확장
메모리 풀링없음PooledByteBufAllocator
참조 카운팅없음ReferenceCounted 인터페이스
제로카피 합성불가CompositeByteBuf
Direct 메모리 관리GC 의존명시적 release()
JAVA
// Mina IoBuffer — ByteBuffer 래퍼의 한계
IoBuffer buf = IoBuffer.allocate(256);
buf.put(data);
buf.flip();  // 빠뜨리면 버그
session.write(buf);

// Netty ByteBuf — flip이 필요 없다
ByteBuf buf = ctx.alloc().buffer(256);
buf.writeBytes(data);  // writerIndex가 자동 증가
ctx.writeAndFlush(buf);  // 전송 후 자동 release

ByteBuf의 참조 카운팅은 고성능 서버에서 GC 부담을 획기적으로 줄여주는 핵심 기능입니다. 이 부분은 시리즈 12편(참조 카운팅)에서 자세히 다뤘습니다.

3. 유연한 스레드 모델

PLAINTEXT
Mina 스레드 모델:
  I/O 스레드 → IoFilter(어디서 실행?) → IoHandler(별도 스레드풀?)
  (스레드 경계가 불명확)

Netty 스레드 모델:
  EventLoop(하나의 스레드) → 해당 Channel의 모든 이벤트 처리
  (Channel과 EventLoop가 1:1로 바인딩, 락 불필요)

Netty의 "하나의 Channel은 하나의 EventLoop에 바인딩된다"는 규칙은 단순하지만 강력합니다. 동기화 없이도 스레드 안전성이 보장되기 때문입니다.

4. 커뮤니티와 생태계

기술적 우위만으로는 부족합니다. Netty가 결정적으로 이긴 건 커뮤니티 입니다.

  • **활발한 개발 **: GitHub에서 지속적인 릴리스와 빠른 버그 수정
  • ** 풍부한 코덱 **: HTTP/1.1, HTTP/2, WebSocket, DNS, SMTP, Redis 프로토콜 등 기본 제공
  • ** 문서화 **: "Netty in Action" 같은 공식 도서, 풍부한 예제 코드
  • ** 대형 프로젝트의 채택 **: Kafka, Elasticsearch 같은 프로젝트가 Netty를 선택하면서 신뢰도가 급상승
  • **Trustin Lee의 풀타임 개발 **: 처음 JBoss(Red Hat)에서, 이후 여러 기업의 지원으로 풀타임 개발 지속

Mina는 Apache 산하 프로젝트이지만 핵심 개발자가 떠나면서 개발 속도가 크게 줄었고, Grizzly는 Oracle 내부 프로젝트 성격이 강해서 외부 커뮤니티가 형성되지 않았습니다.


API 비교 — 같은 기능, 다른 표현

핵심 개념 매핑

MinaNetty설명
IoSessionChannel하나의 네트워크 연결
IoFilterChainChannelPipeline이벤트가 흐르는 처리 체인
IoFilterChannelHandler체인의 개별 처리 단위
IoHandlerChannelHandler비즈니스 로직 (Netty는 통합)
IoAcceptorServerBootstrap서버 바인딩
IoConnectorBootstrap클라이언트 연결
IoBufferByteBuf데이터 버퍼
WriteFutureChannelFuture비동기 작업 결과

Mina에서는 IoFilterIoHandler가 분리된 개념이지만, Netty에서는 ChannelHandler 하나로 통합했습니다. 필터 역할이든 비즈니스 로직이든 모두 ChannelHandler입니다. 이 통합이 API를 훨씬 단순하게 만들었습니다.

Echo 서버 비교

Mina Echo 서버:

JAVA
public class MinaEchoServer {
    public static void main(String[] args) throws Exception {
        IoAcceptor acceptor = new NioSocketAcceptor();

        // 필터 체인에 코덱 추가
        acceptor.getFilterChain().addLast("codec",
            new ProtocolCodecFilter(
                new TextLineCodecFactory(StandardCharsets.UTF_8)));

        // 비즈니스 로직은 별도 IoHandler
        acceptor.setHandler(new IoHandlerAdapter() {
            @Override
            public void messageReceived(IoSession session, Object message) {
                session.write(message);  // 에코
            }

            @Override
            public void exceptionCaught(IoSession session, Throwable cause) {
                cause.printStackTrace();
                session.closeNow();
            }
        });

        acceptor.getSessionConfig().setReadBufferSize(2048);
        acceptor.bind(new InetSocketAddress(8080));
    }
}

Netty Echo 서버:

JAVA
public class NettyEchoServer {
    public static void main(String[] args) throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 protected void initChannel(SocketChannel ch) {
                     ch.pipeline()
                       .addLast(new LineBasedFrameDecoder(1024))
                       .addLast(new StringDecoder(StandardCharsets.UTF_8))
                       .addLast(new StringEncoder(StandardCharsets.UTF_8))
                       .addLast(new SimpleChannelInboundHandler<String>() {
                           @Override
                           protected void channelRead0(
                               ChannelHandlerContext ctx, String msg) {
                               ctx.writeAndFlush(msg + "\n");  // 에코
                           }

                           @Override
                           public void exceptionCaught(
                               ChannelHandlerContext ctx, Throwable cause) {
                               cause.printStackTrace();
                               ctx.close();
                           }
                       });
                 }
             });

            // 서버 시작 및 종료 대기
            ChannelFuture f = b.bind(8080).sync();
            f.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

코드 양은 Netty가 약간 더 많지만, **Boss/Worker 스레드 그룹 분리 **, ** 파이프라인 구성의 명확함 , ** 리소스 정리의 명시성 같은 장점이 눈에 띕니다.


채택 사례 비교

세 프레임워크의 현재 위치를 채택 사례로 비교하면 격차가 확연합니다.

Netty — 사실상 Java 네트워크의 표준

프로젝트용도
Apache Kafka브로커 간 통신, 클라이언트-브로커 통신
Elasticsearch노드 간 트랜스포트, REST API 처리
gRPC-JavaHTTP/2 기반 RPC 전송 계층
Spring WebFlux리액티브 웹 서버 (Reactor Netty)
Apache Cassandra클라이언트-서버 통신
Zuul 2Netflix API Gateway
Vert.x리액티브 애플리케이션 플랫폼의 핵심
ArmeriaLINE의 비동기 RPC/REST 프레임워크
Play Framework비동기 웹 프레임워크
FinagleTwitter의 RPC 시스템

이 목록을 보면, ** 현대 Java 생태계에서 고성능 네트워크가 필요한 거의 모든 프로젝트가 Netty를 선택 **했다는 걸 알 수 있습니다.

Apache Mina — 레거시 생태계

프로젝트용도
Apache SSHDJava SSH 서버/클라이언트 구현
Apache FtpServerJava FTP 서버
Apache DirectoryLDAP 서버

Mina 기반 프로젝트들은 대부분 Apache 재단 내부의 프로토콜 구현체 입니다. 신규 프로젝트에서 Mina를 선택하는 경우는 거의 없고, 기존 프로젝트들도 일부는 Netty로 마이그레이션을 고려하고 있습니다.

Oracle Grizzly — GlassFish 생태계

프로젝트용도
GlassFish/Payara애플리케이션 서버 HTTP 엔진
JerseyJAX-RS 테스트 컨테이너

Grizzly의 사용 범위는 사실상 GlassFish 생태계에 국한됩니다.


정량적 비교

세 프레임워크의 현재 상태를 수치로 비교하면 다음과 같습니다 (2026년 기준 대략적 수치).

항목NettyMinaGrizzly
GitHub Stars~33,000+~900+~200+
최근 릴리스 주기월 단위연 단위불규칙
Maven 의존 프로젝트 수수만 개수백 개수십 개
지원 프로토콜 (내장)HTTP/1.1, HTTP/2, WebSocket, DNS, SMTP, Redis...텍스트/바이너리 코덱 기본HTTP, WebSocket
네이티브 트랜스포트epoll, kqueue, io_uring없음없음
스레드 모델EventLoop (락-프리)IoProcessor 풀Worker 스레드 풀

왜 Netty가 이겼는지 한 문장으로

"올바른 추상화(Channel + Pipeline + EventLoop)를 제공하고, 가장 활발한 커뮤니티가 그 위에 생태계를 쌓았기 때문이다."

기술적으로는 세 프레임워크 모두 NIO 위에서 비동기 네트워크 프로그래밍을 가능하게 했습니다. 하지만 Netty만이 다음 세 가지를 동시에 충족했습니다.

  1. ** 사용하기 쉬운 일관된 API** — Mina보다 단순하고, Grizzly보다 범용적
  2. ** 성능을 타협하지 않는 설계** — ByteBuf, 참조 카운팅, 네이티브 트랜스포트
  3. ** 커뮤니티 주도의 지속적 발전** — 특정 기업이나 재단에 종속되지 않은 독립 프로젝트

만약 지금 Mina/Grizzly를 쓰고 있다면

기존 프로젝트에서 Mina나 Grizzly를 사용하고 있다면, 무조건 Netty로 마이그레이션해야 하는 건 아닙니다. 하지만 다음 상황에서는 전환을 고려할 필요가 있습니다.

  • ** 새로운 프로토콜 지원이 필요할 때 **: HTTP/2, gRPC 같은 현대 프로토콜의 Netty 코덱은 잘 검증되어 있습니다
  • ** 성능 병목이 네트워크 계층에 있을 때 **: ByteBuf 풀링과 네이티브 트랜스포트가 차이를 만듭니다
  • ** 라이브러리 호환성이 필요할 때 **: 대부분의 Java 네트워크 라이브러리가 Netty 기반입니다

마이그레이션 시 개념 매핑은 비교적 직관적입니다. IoSessionChannel, IoFilterChannelHandler, IoFilterChainChannelPipeline으로 대응됩니다.


시리즈를 마치며 — 38편의 여정

이 글로 Netty 시리즈 38편이 완결됩니다. 처음부터 여기까지의 흐름을 정리해 보겠습니다.

기초 (1~6편)

java.net과 java.nio의 한계 에서 시작해서, Netty의 핵심 구성 요소를 하나씩 쌓아 올렸습니다.

  • 왜 NIO를 직접 쓰면 안 되는지 (1편)
  • Bootstrap, EventLoopGroup, Channel, ChannelPipeline, ChannelHandler (2~6편)

이 여섯 가지 개념만 알면 Netty로 기본적인 서버/클라이언트를 만들 수 있습니다.

ByteBuf & 메모리 (7~11편)

Netty가 메모리를 어떻게 효율적으로 다루는지 깊이 파고들었습니다.

  • ByteBuf의 기초와 심화 (7~8편)
  • 참조 카운팅과 메모리 릭 탐지 (9~10편)
  • PooledByteBufAllocator와 메모리 풀 (11편)

고성능 서버를 운영하려면 이 부분을 반드시 이해해야 합니다. GC 튜닝보다 참조 카운팅 누수를 잡는 게 먼저입니다.

코덱 & 프로토콜 (12~14편)

데이터를 ** 어떻게 인코딩/디코딩하고, TCP 프레이밍 문제를 해결하는지** 정리했습니다.

  • 인코더/디코더 기초와 고급 패턴 (12~13편)
  • TCP 점착/분할 패킷 문제 (14편)

내부 동작 & 성능 (15~19편)

Netty 내부의 ** 스레드 모델, Zero-Copy, Flow Control** 같은 핵심 메커니즘을 분석했습니다.

  • 네이티브 트랜스포트, EventLoop 내부 동작 (15~16편)
  • Zero-Copy & FileRegion (17편)
  • Flow Control & Backpressure (18편)
  • 스레드 모델 심화 (19편)

운영 패턴 (20~26편)

실제 서비스에서 Netty를 운영할 때 필요한 ** 예외 처리, 타임아웃, 재연결, 테스트, Graceful Shutdown** 패턴을 다뤘습니다.

  • 유틸리티, 예외 처리, 타임아웃 (20~22편)
  • Graceful Shutdown, 재연결 패턴 (23~24편)
  • EmbeddedChannel 테스트 (25편)
  • 커스텀 프로토콜 설계 (26편)

프로토콜 구현 & 심화 (27~38편)

HTTP/1.1, SSL/TLS, WebSocket, HTTP/2 같은 ** 실전 프로토콜 구현 **을 거쳐, 성능 튜닝과 프레임워크 비교까지 도달했습니다.

  • HTTP/1.1, SSL/TLS, WebSocket (27~29편)
  • 부하 테스트 & 성능 튜닝 (30편)
  • HTTP/2 멀티플렉싱 (31편)
  • 그리고 이 글 — Netty vs Mina vs Grizzly (38편)

다음 학습 방향

Netty의 기초부터 심화까지 정리했으니, 여기서 갈 수 있는 방향은 여러 가지입니다.

  • **gRPC-Java 소스 읽기 **: Netty 위에서 HTTP/2 + Protobuf RPC가 어떻게 구현되는지 실제 코드로 확인
  • **Reactor Netty / Spring WebFlux 내부 탐구 **: 리액티브 프로그래밍과 Netty가 어떻게 결합되는지
  • ** 직접 프로토콜 설계 **: 26편에서 다룬 커스텀 프로토콜을 실제 프로젝트에 적용
  • **io_uring 트랜스포트 **: Netty의 차세대 리눅스 I/O 트랜스포트
  • **Netty 소스 코드 읽기 **: NioEventLoop.run(), PooledByteBufAllocator 내부 구현을 직접 따라가기

38편에 걸쳐 정리한 내용이 면접 준비든, 실무 적용이든, Netty를 이해하는 데 도움이 되었으면 좋겠습니다.

댓글 로딩 중...