JAVA/비동기 프로그래밍

[Netty] Netty 개요 및 간단한 예제

꾸준함. 2024. 7. 27. 02:49

Netty

  • 비동기 이벤트 기반의 네트워크 애플리케이션 프레임워크
  • HTTP뿐만 아니라 FTP, SMTP, Telnet 등 다양한 네트워크 프로토콜을 지원
  • Java IO, NIO, Selector 기반으로 적은 리소스로 높은 성능 보장
    • NIO는 비동기 I/O를 지원하여, 블로킹 방식의 IO에 비해 더 나은 성능을 제공
    • NIO의 Selector는 여러 채널에서 발생하는 I/O 이벤트를 감지하고 처리
    • 불필요한 메모리 복사를 최소화하여 메모리 사용을 최적화

 

  • 이벤트 모델은 매우 유연하고 확장 가능하며 필요에 따라 커스텀 이벤트 핸들러를 추가하거나 제거할 수 있음
  • 서버와 클라이언트 모두 지원하며 이를 통해 양방향 통신이 필요한 애플리케이션을 쉽게 개발할 수 있음

 

https://netty.io/

 

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로

 

https://www.getoutsidedoor.com/2022/07/23/channel-concept-implementation-el-project-2/

 

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