이벤트 루프 기반 프레임워크 동작 방식은 단일 스레드 이벤트 루프와 다중 스레드 이벤트 루프로 나눌 수 있습니다.
Netty 동작 방식 이해를 위해 두 가지 차이점을 비교해보겠습니다.
단일 스레드 이벤트 루프
이는 이벤트를 처리하는 스레드가 하나인 상태를 의미합니다.
- 이벤트 루프의 구현이 단순하고 예측 가능한 동작을 보장합니다.
- 하나의 스레드가 이벤트 큐에 입력된 이벤트를 처리하므로 이벤트가 발생한 순서대로 처리할 수 있습니다.
- 단점 1 - 다중 코어 CPU를 효율적으로 사용하지 못함
- 단점 2 - 이벤트 메서드에 처리 시간이 오래 걸리는 작업이 있는 경우 다음 이벤트 처리 지연 발생
대표적인 프레임워크 - Node.js
다중 스레드 이벤트 루프
이벤트를 처리하는 스레드가 여러개입니다.
- 단일 스레드 이벤트 루프에 비해 프레임워크의 구현이 복잡합니다
- 이벤트 메서드를 병렬 수행하여 다중 코어 CPU를 효율적으로 사용합니다
- 장점 - 시간이 많이 걸리는 작업을 여러 스레드로 분할처리하여 전체 처리 시간을 단축시킬 수 있습니다
- 단점 1 - 여러 이벤트 루프 스레드가 이벤트 큐 하나에 접근하기때문에 스레드 경합이 발생합니다
- 단점 2 - 이벤트의 발생 순서와 실행순서를 보장할 수 없습니다. 즉, 이벤트 발생순서와 실행순서 불일치가 발생
다중 스레드 이벤트 루프 사용시 주의점
- 스레드 개수를 너무 많이 설정하거나 개수를 제한하지 않으면 과도한 GC의 원인이 되거나 OOM을 발생시킬 수 있습니다.
- 스레드 경합과 컨텍스트 스위칭으로 인한 성능 저하가 발생할 수 있습니다
스레드 개수가 적을 때는 다중 스레드의 장점을 얻을 수있으나, 스레드 개수가 지속적으로 증가하면 장점보다는 스레드 경합에 의한 성능 저하가 발생할 수 있으므로 애플리케이션 부하 테스트를 통해 적정한 스레드 개수 설정을 해야 합니다.
Netty의 이벤트 루프
네티는 단일 스레드 이벤트 루프와 다중 스레드 이벤트 루프를 모두 지원하고, 이벤트 루프의 종류에 상관없이 이벤트 발생 순서에 따른 실행 순서를 보장합니다. 역시 Netty를 많이 사용하는 이유가 있습니다.
Netty에서 어떻게 이벤트 발생 순서와 실행순서를 일치시킬 수 있을까요?
Netty는 이벤트 루프들이 이벤트 큐를 공유할 수 없도록, 이벤트 루프 스레드 내부에 이벤트 큐를 두었습니다.
이것이 어떻게 가능한가?
- 네티의 이벤트는 채널에서 발생
- 이벤트 루프 객체는 이벤트 큐를 가진다
- 네티의 채널은 하나의 이벤트 루프에 등록된다
이 특징을 이용하여 각자의 이벤트 큐를 가진 이벤트 루프 스레드를 여러개 만들어 냅니다.
여러 채널이 하나의 이벤트 루프에 등록되어도 이벤트 처리는 항상 발생 순서와 같습니다.
관련 클래스 - SingleThreadEventExecutor(이벤트를 task라는 이름으로 선언하여 처리함)
퓨처(Future) 패턴 알아보기
네티는 비동기 호출을 위해 리액터 패턴의 구현체인 EventHandler 와 퓨처 패턴의 구현체인 ChannelFuture 를 사용합니다. 이번에는 ChannelFuture에 대해 알아보겠습니다.
그전에 Future 패턴에 대해 간단히 알아보자면
Future패턴은 메서드를 호출하는 즉시 Future 객체를 돌려줍니다. 그리고 메서드의 처리 결과는 나중에 Future객체를 통해서 확인합니다.(카페에서 대기표를 가지고 확인하는 것과 비슷한 동작)
Netty에선느 비동기 I/O 메서드 호출의 결과로 ChannelFuture 객체를 돌려받게 되고,
이 객체를 통해서 작업의 완료 유무를 확인할 수 있습니다.
그리고 ChannelFuture 객체에 작업이 완료되었을 때 수행할 채널 리스너를 설정할 수 있습니다.
아래는 에코서버를 생성하는 간단한 예제코드인데, 여기서도 ChannelFuture를 사용하고 있습니다.
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) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerV4FirstHandler()); // 수신된 데이터를 처리할 핸들러 지정
p.addLast(new EchoServerV4SecondHandler());
}
});
ChannelFuture channelFuture = b.bind(12000).sync();
channelFuture.channel().closeFuture().sync();
}finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
ChannelFuture channelFuture = b.bind(12000).sync();
1. bind(int inetPort): 포트를 바인드하는 비동기 메서드
2. sync() : 주어진 ChannelFuture 객체의 작업이 완료될 때까지 블로킹하는 메서드.
channelFuture.channel().closeFuture().sync();
1. channel() : channelFuture 객체를 통해서 채널(포트바인딩된 서버채널)을 얻어오는 메서드
2. closeFuture() : 채널의 연결이 종료될 때 연결종료 이벤트를 받는 CloseFuture 객체를 받습니다. (네티 내부에서는 채널이 생성될 때 CloseFuture객체도 같이 생성됩니다 )
아래는 ChannelFuture에 채널 리스너 설정하는 예제입니다
public class EchoServerV5Handler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ChannelFuture channelFuture = ctx.writeAndFlush(msg);
channelFuture.addListener(ChannelFutureListener.CLOSE); // 연결된 소켓 채널을 닫는다
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
이 핸들러로 Server 실행하면 msg 보낸 후에 client와 연결된 소켓 채널을 닫습니다.
channelFuture.addListener(ChannelFutureListener.CLOSE); // 연결된 소켓 채널을 닫는다
위 코드는 ChannelFuture 객체에 채널을 종료하는 리스너를 등록하는 코드입니다.
ChannelFutureListener.CLOSE 리스너는 네티가 제공하는 기본 리스너인데, ChannelFuture객체가 완료 이벤트를 수신할 때 수행됩니다.
Netty가 제공하는 기본 채널 리스너
- ChannelFutureListener.CLOSE
- 시점 - ChannelFuture 객체가 작업 완료 이벤트를 수신했을 때
- ChannelFuture객체에 포함된 채널을 닫는다(작업 성공 여부와 상관 없이 수행)
- ChannelFutureListener.CLOSE_ON_FAILURE
- 시점 - ChannelFuture 객체가 완료 이벤트를 수신하고 결과가 실패일 때
- ChannelFuture 객체에 포함된 채널을 닫는다
- ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE
- 시점 - ChannelFuture 객체가 완료 이벤트를 수신하고 결과가 실패일 때
- 채널 예외 이벤트를 발생시킨다
ChannelFutureListener 인터페이스를 구현한 클래스를 작성하여 사용자 정의 채널 리스너를 구현할 수 도 있다.
사용 예시) 클라이언트 소켓 채널에 데이터 기록이 완료되었을 때 기록 완료 메시지를 출력하고 소켓 닫기
참고자료 : 자바 네트워크 소녀 Netty - 정경석
'Java > Netty' 카테고리의 다른 글
ChannelGroup과 GlobalEventExecutor (0) | 2024.07.01 |
---|---|
TCP Keepalive (0) | 2024.06.24 |