Netty
- 비동기 이벤트 기반의 네트워크 애플리케이션 프레임워크
- HTTP뿐만 아니라 FTP, SMTP, Telnet 등 다양한 네트워크 프로토콜을 지원
- Java IO, NIO, Selector 기반으로 적은 리소스로 높은 성능 보장
- NIO는 비동기 I/O를 지원하여, 블로킹 방식의 IO에 비해 더 나은 성능을 제공
- NIO의 Selector는 여러 채널에서 발생하는 I/O 이벤트를 감지하고 처리
- 불필요한 메모리 복사를 최소화하여 메모리 사용을 최적화
- 이벤트 모델은 매우 유연하고 확장 가능하며 필요에 따라 커스텀 이벤트 핸들러를 추가하거나 제거할 수 있음
- 서버와 클라이언트 모두 지원하며 이를 통해 양방향 통신이 필요한 애플리케이션을 쉽게 개발할 수 있음

NIOEventLoop
- Netty 프레임워크에서 비동기 I/O 작업을 수행하는 주요 구성 요소 중 하나로, 이벤트 루프(EventLoop) 모델을 구현한 클래스
- 네트워크 I/O 작업을 처리하고, 이벤트를 감지하며, 이벤트에 대응하는 핸들러를 실행
- Java NIO 라이브러리를 기반으로 비동기적으로 I/O 작업을 처리
- 기본적으로 단일 스레드로 동작하며 하나의 쓰레드가 여러 채널을 처리
- 쓰레드 간 컨텍스트 스위칭 비용을 줄이고 성능을 최적화
- Selector를 사용하여 다중 채널의 I/O 이벤트를 감지하고 처리
- Selector는 여러 채널에서 발생하는 이벤트를 감지하고, 이를 하나의 쓰레드에서 처리
1. NIOEventLoop의 구성 요소
- EventLoop: Netty의 이벤트 루프 인터페이스로 I/O 작업을 수행하고 이벤트를 처리
- NIOEventLoop: EventLoop의 구현체로 Java NIO 기반의 비동기 I/O 작업을 처리
- EventLoopGroup: EventLoop의 그룹을 나타내는 인터페이스로 Netty는 이러한 이벤트 루프 그룹을 통해 네트워크 애플리케이션의 성능을 최적화하고 높은 동시성을 제공
- EventLoopGroup은 여러 EventLoop 인스턴스를 포함하고 있으며, 각 EventLoop는 하나 이상의 Channel을 처리
- 각각의 EventLoop에 순차적으로 task가 추가되고 실행하기 때문에 EventExecutor 단위로 놓고 보면 순서 보장
- EventExecutor: task를 실행하는 쓰레드풀
- TaskQueue: EventLoop은 I/O 작업 외에도 다양한 task를 처리할 수 있도록 TaskQueue를 가지고 있으며 EventExecutor가 즉시 task를 수행하지 않고 TaskQueue에 넣은 후 나중에 꺼내서 처리하도록 지원
- Selector: 다중 채널의 I/O Multiplexing을 지원
2. NIOEventLoop의 task
- I/O task와 Non I/O task로 구분
- I/O task: register를 통해 내부의 selector에 채널을 등록하고 I/O 준비 완료 이벤트 발생 시 채널의 파이프라인 실행
- Non I/O task: TaskQueue에 Runnable 등 실행 가능한 모든 종류의 task를 꺼내 실행
- ioRatio 설정을 통해 task 수행에 얼마나 시간을 쓸지 정할 수 있으며 default 값은 50
- 50이면 I/O task와 Non I/O task에 최대한 동일한 시간을 소모
- 높은 ioRatio 값을 설정하면, 이벤트 루프가 더 많은 시간을 I/O 작업에 할애하게 되어, I/O 작업의 처리 속도가 향상될 수 있지만 Non I/O 작업이 지연될 수 있음
- 반대로 낮은 ioRatio 값을 설정하면, 이벤트 루프가 더 많은 시간을 비 I/O 작업에 할애하게 되어, 비 I/O 작업의 처리 속도가 향상될 수 있지만 I/O 작업의 처리 속도가 저하될 수 있음
3. NIOEventLoop의 동작 과정
- NIOEventLoop는 Selector에 채널을 등록하여 해당 채널에서 발생하는 I/O 이벤트 감지를 하며 이 과정은 주기적으로 Selector.select() 메서드를 호출하여 이루어짐
- 감지된 I/O 이벤트는 해당 이벤트에 대응하는 핸들러로 전달됨
- EventLoop는 I/O 이벤트 처리 외에도 다양한 task를 실행할 수 있으며 TaskQueue에 제출된 task는 EventLoop 내에서 순차적으로 실행
Channel
1. ChannelFuture
- Netty 프레임워크에서 비동기 I/O 작업의 결과를 나타내는 인터페이스
- Netty는 대부분의 I/O 작업이 비동기로 수행되기 때문에, 작업이 완료되기 전에는 그 결과를 즉시 확인할 수 없는데 ChannelFuture는 이러한 비동기 작업의 결과를 확인하고, 완료 시점에 콜백을 실행하는 데 사용
- ChannelFuture는 효율적인 I/O 작업 처리와 높은 동시성을 제공하는 핵심 요소 중 하나
2. NioServerSocketChannel
- Netty 프레임워크에서 비동기 서버 소켓을 구현한 클래스
- Java NIO 라이브러리를 기반으로 하여 비동기적으로 클라이언트 연결을 수락하고 관리할 수 있도록 설계되어 있음
- Java NIO의 Selector를 사용하여 여러 클라이언트 연결을 효율적으로 관리
- Netty의 EventLoopGroup과 함께 사용되어, 여러 스레드를 통해 클라이언트 요청을 병렬로 처리
- 수백만 개의 연결을 처리할 수 있는 고성능 서버 애플리케이션을 구축하는 데 적합

부연 설명
- AbstractChannel: 채널 Pipeline을 가짐
- AbstractNioChannel: 내부적으로 ServerSocketChannel을 저장하고 register 할 때 Selector에 등록
3. ChannelPipeline
- Netty는 이벤트 기반의 네트워크 프레임워크로 데이터의 입출력 및 네트워크 이벤트 처리는 여러 단계로 이루어지는데, 이 단계들을 정의하고 관리하는 것이 ChannelPipeline
- Netty의 중요한 구성 요소로, 네트워크 이벤트를 처리하는 핸들러의 체인을 관리
- 여러 개의 ChannelHandler를 순차적으로 호출하여, 네트워크 이벤트를 처리하며 ChannelHandler는 입출력 데이터의 변환, 로깅, 인증 등 다양한 작업을 수행할 수 있음
- 네트워크 이벤트는 ChannelPipeline을 통해 흐르며, 각 핸들러가 이벤트를 처리하거나 다음 핸들러로 전달할 수 있음
- ChannelPipeline은 실행 중에 핸들러를 동적으로 추가, 제거, 교체할 수 있으며 이를 통해 유연성을 높임
- ChannelPipeline은 인바운드(inbound)와 아웃바운드(outbound) 이벤트를 구분하여 처리
- 인바운드 이벤트는 클라이언트로부터 들어오는 데이터와 관련된 이벤트
- 아웃바운드 이벤트는 서버에서 클라이언트로 나가는 데이터와 관련된 이벤트
3.1 ChannelPipeline 동작 과정
- 네트워크 이벤트가 발생하면, ChannelPipeline은 해당 이벤트를 첫 번째 ChannelHandler로 전달
- 각 핸들러는 이벤트를 처리하고, 다음 핸들러로 이벤트를 전달할지 결정
- 인바운드 이벤트는 channelRead와 같은 메서드를 통해 처리
- 아웃바운드 이벤트는 write와 같은 메서드를 통해 처리
- 이벤트는 ChannelPipeline을 따라 흐르며, 각 핸들러에서 처리되며 필요에 따라 이벤트는 중단되거나 수정될 수 있음
- 실행 중에 동적으로 핸들러를 추가하거나 제거할 수 있으며 이를 통해 런타임에서의 유연한 구성을 지원
3.2 ChannelPipeline 내부
- ChannelPipeline은 ChannelHandlerContext의 연속
- Head context와 Tail context는 기본적으로 포함하며 각각의 context는 DoublyLinkedList 형태로 next, prev를 통해 이전 혹은 다음 context에 접근 가능
- 모든 인바운드 I/O 이벤트는 next로
- 모든 아웃바운드 I/O 작업은 prev로

3.3 ChannelHandlerContext에서 별도의 EventExecutor를 지원하는 이유
- Netty에서 ChannelHandler는 네트워크 이벤트를 처리하는 역할을 수행하며 이때, ChannelHandlerContext를 통해 이벤트를 처리하거나 다음 핸들러로 이벤트를 전달
- 그러나 ChannelHandler에서 시간이 오래 걸리는 연산을 수행하면, EventLoop 쓰레드가 블로킹되어 다른 채널의 I/O 처리가 지연될 수 있음
- 이를 방지하기 위해 Netty는 ChannelHandlerContext에서 별도의 EventExecutor를 지원
- ChannelHandlerContext에 별도의 EventExecutor를 등록하여, 특정 핸들러가 다른 쓰레드풀에서 동작하도록 설정할 수 있으며 이를 통해 EventLoop 쓰레드는 블로킹 없이 계속해서 다른 채널의 I/O 이벤트를 처리할 수 있음
- ChannelHandler에서 오래 걸리는 작업을 직접 처리하지 않고, executor.execute() 메서드를 통해 TaskQueue에 작업을 제출
- 제출된 작업은 별도의 EventExecutor 쓰레드에서 실행되며 EventLoop 쓰레드는 즉시 다음 작업을 처리할 수 있음
- 작업이 완료되면, 결과를 처리하고 필요한 경우 다음 핸들러로 이벤트를 전달 (쓰레드가 다르면 next executor의 TaskQueue에 이벤트 처리를 추가하고 쓰레드가 같다면 해당 쓰레드가 직접 이벤트 처리를 실행)
4. ChannelHandler
- Netty 프레임워크에서 네트워크 이벤트를 처리하는 기본 인터페이스
- ChannelHandler는 ChannelPipeline에 추가되어, 네트워크 이벤트(데이터 수신, 데이터 송신, 예외 발생 등)를 처리하고 조작
- Netty는 여러 종류의 ChannelHandler를 제공하며, 개발자는 이를 확장하여 커스텀 핸들러를 작성할 수 있음
4.1 ChannelInboundHandler
- 인바운드 이벤트(데이터 수신, 연결 수락 등)를 처리하는 핸들러
- 주요 메서드는 다음과 같음
- channelRead: 데이터를 수신했을 때 호출
- channelActive: 채널이 활성화되었을 때 호출
- channelInactive: 채널이 비활성화되었을 때 호출
- exceptionCaught: 예외가 발생했을 때 호출
4.2 ChannelOutboundHandler
- 아웃바운드 이벤트(데이터 송신, 연결 요청 등)를 처리하는 핸들러
- 주요 메서드는 다음과 같음
- write: 데이터를 송신할 때 호출
- flush: 데이터를 플러시 할 때 호출
- connect: 원격지에 연결할 때 호출
- disconnect: 원격지와 연결을 해제할 때 호출
4.3 ChannelDuplexHandler
- ChannelInboundHandler와 ChannelOutputHandler를 모두 구현한 핸들러
Netty로 EchoServer 구현한 예제
1. NettyEchoServer
- Netty를 사용하여 에코 서버를 설정하는 예제
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class NettyEchoServer { | |
@SneakyThrows | |
public static void main(String[] args) { | |
EventLoopGroup parentGroup = new NioEventLoopGroup(); | |
EventLoopGroup childGroup = new NioEventLoopGroup(4); | |
EventExecutorGroup eventExecutorGroup = new DefaultEventLoopGroup(4); | |
try { | |
ServerBootstrap serverBootstrap = new ServerBootstrap(); | |
var server = serverBootstrap | |
.group(parentGroup, childGroup) | |
.channel(NioServerSocketChannel.class) | |
.childHandler(new ChannelInitializer<>() { | |
@Override | |
protected void initChannel(Channel ch) throws Exception { | |
ch.pipeline().addLast( | |
eventExecutorGroup, new LoggingHandler(LogLevel.INFO) | |
); | |
ch.pipeline().addLast( | |
new StringEncoder(), | |
new StringDecoder(), | |
new NettyEchoServerHandler() | |
); | |
} | |
}); | |
server.bind(8080).sync() | |
.addListener(new FutureListener<>() { | |
@Override | |
public void operationComplete(Future<Void> future) throws Exception { | |
if (future.isSuccess()) { | |
log.info("Success to bind 8080"); | |
} else { | |
log.error("Fail to bind 8080"); | |
} | |
} | |
}).channel().closeFuture().sync(); | |
} finally { | |
parentGroup.shutdownGracefully(); | |
childGroup.shutdownGracefully(); | |
eventExecutorGroup.shutdownGracefully(); | |
} | |
} | |
} |
코드 부연 설명
- parentGroup: 클라이언트 연결을 수락하는 이벤트 루프 그룹
- childGroup: 클라이언트와의 실제 I/O 작업을 처리하는 이벤트 루프 그룹이며 쓰레드 수를 4개로 설정
- eventExecutorGroup: 시간이 오래 걸리는 작업을 처리하기 위한 별도의 이벤트 실행 그룹이며 쓰레드 수를 4개로 설정
- serverBootstrap: 서버 설정을 위한 도우미 객체
- server.channel(NioServerSocketChannel.class): 서버 소켓 채널 유형을 NIO 기반으로 설정
- server.childHandler(new ChannelInitializer<>()): 새로운 채널이 생성될 때 호출될 초기화 핸들러를 설정
- ch.pipeline().addLast(eventExecutorGroup, new LoggingHandler(LogLevel.INFO)): 파이프라인에 로깅 핸들러를 추가
- ch.pipeline().addLast(new StringEncoder(), new StringDecoder(), new NettyEchoServerHandler()): 파이프라인에 문자열 인코더, 디코더, 커스텀 에코 서버 핸들러를 추가
- server.bind(8080).sync(): 서버를 포트 8080에 바인딩하고, 비동기로 실행시키며 sync()는 바인딩이 완료될 때까지 현재 쓰레드를 block 시킴
- .addListener(new FutureListener<>()): 바인딩 결과를 비동기로 처리하는 리스너를 추가
- operationComplete(Future<Void> future): 바인딩 작업이 완료되었을 때 호출
- .channel().closeFuture().sync(): 서버 채널이 닫힐 때까지 대기
2. NettyEchoServerHandler
- Netty 프레임워크를 사용하여 간단한 에코 서버의 핸들러를 구현한 예제
- 해당 핸들러는 클라이언트로부터 수신한 메시지를 다시 클라이언트에게 돌려보내는 역할
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class NettyEchoServerHandler extends ChannelInboundHandlerAdapter { | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { | |
if (msg instanceof String) { | |
log.info("Received: {}", msg); | |
ctx.writeAndFlush(msg) | |
.addListener(ChannelFutureListener.CLOSE); | |
} | |
} | |
} |
코드 부연 설명
- ChannelInboundHandlerAdapter: Netty의 기본 핸들러 구현체로, 필요한 메서드만 오버라이드하여 사용할 수 있음
- channelRead: Netty에서 수신된 메시지를 처리하는 메서드로 네트워크에서 데이터를 읽을 때마다 호출
- ChannelHandlerContext ctx: 채널 핸들러와 파이프라인, 채널 등과 상호작용할 수 있는 컨텍스트 객체로 이벤트를 전파하거나 데이터를 전송할 수 있음
- ctx.writeAndFlush(msg)를 사용하여 수신된 메시지를 클라이언트에게 다시 전송
- .addListener(ChannelFutureListener.CLOSE)를 사용하여 메시지 전송 후 채널을 닫는 리스너를 추가
3. 에코 서버의 전체적인 흐름
- 클라이언트 연결을 수락하고, I/O 작업을 처리할 EventLoopGroup을 생성
- 서버 설정을 구성하고, 이벤트 루프 그룹과 채널 유형을 설정
- 새로운 채널이 생성될 때 호출될 초기화 핸들러를 통해 파이프라인을 구성
- 서버를 지정된 포트에 바인딩하고, 연결을 대기
- 클라이언트 연결을 수락하고, 각 연결에 대해 새로운 채널과 파이프라인을 설정
- 수신된 메시지를 로그에 기록하고, 클라이언트에게 다시 전송
- 서버가 종료될 때 EventLoopGroup을 정리하고, 자원을 해제

4. NettyEchoClient
- Netty를 사용하여 에코 클라이언트를 설정하는 예제
- 클라이언트는 서버에 연결하여 메시지를 전송하고, 서버로부터 동일한 메시지를 되돌려 받음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class NettyEchoClient { | |
@SneakyThrows | |
public static void main(String[] args) { | |
EventLoopGroup workerGroup = new NioEventLoopGroup(1); | |
try { | |
Bootstrap bootstrap = new Bootstrap(); | |
var client = bootstrap | |
.group(workerGroup) | |
.channel(NioSocketChannel.class) | |
.handler(new ChannelInitializer<Channel>() { | |
@Override | |
protected void initChannel(Channel ch) throws Exception { | |
ch.pipeline().addLast( | |
new LoggingHandler(), | |
new StringEncoder(), | |
new StringDecoder(), | |
new NettyEchoClientHandler() | |
); | |
} | |
}); | |
client.connect("localhost", 8080).sync() | |
.channel().closeFuture().sync(); | |
} finally { | |
workerGroup.shutdownGracefully(); | |
} | |
} | |
} |
코드 부연 설명
- workerGroup: I/O 작업을 처리하는 이벤트 루프 그룹이며 해당 예제에서는 단일 쓰레드로 작업을 처리
- bootstrap: 클라이언트 부트스트랩 객체로, 클라이언트 설정을 돕는 도우미 클래스
- bootstrap.group(workerGroup): 클라이언트의 이벤트 루프 그룹을 설정하며 workerGroup은 클라이언트의 I/O 작업을 처리
- .channel(NioSocketChannel.class): 클라이언트 소켓 채널 유형을 NIO 기반으로 설정
- .handler(new ChannelInitializer<Channel>() {}): 새로운 채널이 생성될 때 호출될 초기화 핸들러를 설정
- new LoggingHandler(): 로깅 핸들러로, 네트워크 이벤트를 로깅
- new StringEncoder(): 문자열을 바이트로 인코딩하는 핸들러
- new StringDecoder(): 바이트를 문자열로 디코딩하는 핸들러
- new NettyEchoClientHandler(): 커스텀 클라이언트 핸들러로, 에코 클라이언트의 동작을 정의
- client.connect("localhost", 8080).sync(): 지정된 호스트와 포트로 서버에 연결하며 sync()는 연결이 완료될 때까지 현재 쓰레드를 block
- .channel().closeFuture().sync(): 채널이 닫힐 때까지 대기하며 클라이언트의 연결이 유지되는 동안 해당 코드가 실행됨
5. NettyEchoClientHandler
- Netty를 사용하여 클라이언트 측에서 네트워크 이벤트를 처리하는 핸들러를 구현한 예제
- 해당 핸들러는 클라이언트가 서버에 연결될 때 메시지를 전송하고, 서버로부터 수신한 메시지를 로그에 기록하는 역할을 수행
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Slf4j | |
public class NettyEchoClientHandler extends ChannelInboundHandlerAdapter { | |
@Override | |
public void channelActive(ChannelHandlerContext ctx) throws Exception { | |
ctx.writeAndFlush("This is client."); | |
} | |
@Override | |
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { | |
if (msg instanceof String) { | |
log.info("Received: {}", msg); | |
} | |
} | |
} |
코드 부연 설명
- ChannelInboundHandlerAdapter: Netty의 기본 핸들러 구현체로, 필요한 메서드만 오버라이드하여 사용할 수 있음
- channelActive: 채널이 활성화되었을 때 호출되며 클라이언트가 서버와 연결되면 해당 메서드가 호출됨
- ChannelHandlerContext ctx: 채널 핸들러와 파이프라인, 채널 등과 상호작용할 수 있는 컨텍스트 객체이며 이를 통해 이벤트를 전파하거나 데이터를 전송할 수 있음
- ctx.writeAndFlush("This is client."): 클라이언트가 서버와 연결되었을 때 "This is client."라는 문자열 메시지를 서버로 전송하며 writeAndFlush는 메시지를 기록하고 즉시 전송하는 메서드
- channelRead: Netty에서 수신된 메시지를 처리하는 메서드이며 네트워크에서 데이터를 읽을 때마다 호출됨
6. 에코 클라이언트의 전체적인 흐름
- 클라이언트의 I/O 작업을 처리할 단일 쓰레드 EventLoopGroup을 생성
- 클라이언트 부트스트랩을 생성하고, 이벤트 루프 그룹 및 채널 유형을 설정
- 새로운 채널이 생성될 때 초기화 핸들러를 통해 파이프라인에 로깅 핸들러, 문자열 인코더 및 디코더, 커스텀 클라이언트 핸들러를 추가
- 지정된 호스트와 포트로 서버에 연결하고, 연결이 완료될 때까지 대기
- 채널이 닫힐 때까지 대기
- EventLoopGroup을 종료하고 자원을 해제

참고하면 좋은 링크
https://mark-kim.blog/netty_deepdive_1/
참고
- 패스트 캠퍼스 - Spring Webflux 완전 정복 : 코루틴부터 리액티브 MSA 프로젝트까지
반응형
'JAVA > 비동기 프로그래밍' 카테고리의 다른 글
[Java] 자바 IO, NIO, AIO (0) | 2024.07.14 |
---|---|
[Java] CompletableFuture (0) | 2024.05.12 |
[Java] ThreadPoolExecutor (1) | 2024.04.28 |
[Java] 자바 동시성 프레임워크 (4) | 2024.04.20 |
[Java] 동기화 도구 (0) | 2024.03.14 |