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를 사용하여 에코 서버를 설정하는 예제
코드 부연 설명
- 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 프레임워크를 사용하여 간단한 에코 서버의 핸들러를 구현한 예제
- 해당 핸들러는 클라이언트로부터 수신한 메시지를 다시 클라이언트에게 돌려보내는 역할
코드 부연 설명
- ChannelInboundHandlerAdapter: Netty의 기본 핸들러 구현체로, 필요한 메서드만 오버라이드하여 사용할 수 있음
- channelRead: Netty에서 수신된 메시지를 처리하는 메서드로 네트워크에서 데이터를 읽을 때마다 호출
- ChannelHandlerContext ctx: 채널 핸들러와 파이프라인, 채널 등과 상호작용할 수 있는 컨텍스트 객체로 이벤트를 전파하거나 데이터를 전송할 수 있음
- ctx.writeAndFlush(msg)를 사용하여 수신된 메시지를 클라이언트에게 다시 전송
- .addListener(ChannelFutureListener.CLOSE)를 사용하여 메시지 전송 후 채널을 닫는 리스너를 추가
3. 에코 서버의 전체적인 흐름
- 클라이언트 연결을 수락하고, I/O 작업을 처리할 EventLoopGroup을 생성
- 서버 설정을 구성하고, 이벤트 루프 그룹과 채널 유형을 설정
- 새로운 채널이 생성될 때 호출될 초기화 핸들러를 통해 파이프라인을 구성
- 서버를 지정된 포트에 바인딩하고, 연결을 대기
- 클라이언트 연결을 수락하고, 각 연결에 대해 새로운 채널과 파이프라인을 설정
- 수신된 메시지를 로그에 기록하고, 클라이언트에게 다시 전송
- 서버가 종료될 때 EventLoopGroup을 정리하고, 자원을 해제
4. NettyEchoClient
- Netty를 사용하여 에코 클라이언트를 설정하는 예제
- 클라이언트는 서버에 연결하여 메시지를 전송하고, 서버로부터 동일한 메시지를 되돌려 받음
코드 부연 설명
- 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를 사용하여 클라이언트 측에서 네트워크 이벤트를 처리하는 핸들러를 구현한 예제
- 해당 핸들러는 클라이언트가 서버에 연결될 때 메시지를 전송하고, 서버로부터 수신한 메시지를 로그에 기록하는 역할을 수행
코드 부연 설명
- 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 |