자바 네트워크 소녀 Netty 정리

·2021년 9월 11일
14

Netty

목록 보기
1/1
post-thumbnail

이 책을 공부하는 이유

대규모 트래픽을 좀 더 효율적으로 처리할 수 있는 서버를 개발하기 위해, 많은 사람들이 기존 Tomcat + Spring MVC 환경에서 Netty + Spring Webflux 전환을 고려하고 있다.
각각의 환경의 장단점이 있고 상호보완적으로 쓰일 수 있다.

Netty 란 무엇일까? 근본적으로 비동기+논블로킹+이벤트기반 프로그래밍이란 뭘까? 라는 물음에 답하기 위해 책을 찾아봤고 회사 선배의 추천을 받아 '자바 네트워크 소녀 Netty' 를 공부하게 되었다.
이 책의 내용을 기반으로 Netty의 동작원리, 개념에 대해 정리하고자 한다.


네티를 왜 쓰는가?

1장에서는 본격적으로 네티의 개념에 대해 알아보기 전, 간단한 echo 서버, 클라이언트를 구현해본다.

네티는 자바 네트워크 프레임워크로서 자바환경에서 네트워크 프로그래밍을 손쉽게 할 수 있게 해주며 고성능, 고안정성의 프로그램을 만들 수 있게 해준다.

네트워크 어플리케이션을 그냥 자바의 소켓 프로그래밍을 통해 구현하는 것에 비해 네티의 추상화로 인해 얼마나 편하고 간결하게 자바 네트워크 프로그래밍을 할 수 있는지를 보여준다.

네티에서 데이터 이동의 방향성

네티는 이벤트를 Inbound 이벤트, Outbound 이벤트로 구분한다.

클라이언트로 부터 데이터 수신할 때 발생하는 이벤트에 관심이 있다면, Inbound 이벤트 중 하나인 '데이터 수신 이벤트'를 담당하는 메서드에 원하는 로직을 넣으면 된다.

네티는 기본적으로, 적절한 추상화 모델을 제공하여 개발자가 간단한 코드작성만으로 안정적이고 빠른 네트워크 어플리케이션을 개발할 수 있게 도와준다.

  • 추상화
  • 안정적
  • 빠름

네티의 이벤트 모델

네티는 '네트워크 송수신' 을 추상화하기 위해 이벤트 모델을 정의했다.

  • 데이터 송신 -> 아웃바운드 이벤트
  • 데이터 수신 -> 인바운드 이벤트
 Client ---------------------------------------------------------Server

 Outbound        ----->  Send    (몇시에요?) ---------->         Inbound

 Inbound         <–--- (오후 8시) Response <-----------         Outbound

2장. 네티의 주요 특징

네티의 주요 키워드

  • 비동기
  • 이벤트 기반
  • 고성능
  • 추상화

동기와 비동기, 블로킹과 논블로킹

함수 또는 서비스의 호출방식

개념 : https://deveric.tistory.com/99

비동기호출을 지원하는 디자인 패턴

  • 티켓을 활용한 퓨처패턴
  • 이벤트 리스너 : 옵저버 패턴
  • Node.js : 콜백 함수
  • Netty : 리액터 패턴

자바에서 블로킹 소켓 : ServerSocket, Socket

블로킹 소켓을 사용하면, 클라이언트가 서버로 연결요청을 할 때 서버가 연결을 수락하고 클라이언트와 연결된 소켓을 새로 생성하는데 이때 처리가 완료될 때 까지 스레드의 블로킹이 발생한다.

  • read, write, accept 같은 입출력 메서드에서 스레드의 블로킹 발생

다중 클라이언트 처리를 위해 클라이언트 마다 소켓 생성후 그 소켓을 새로 생성된 스레드에 할당해주는 개선안이 사용된다. (클라이언트 마다 스레드 생성)

하지만 이 경우에도 결국은 ServerSocket 의 accept 메서드가 병목지점이 된다.
동시에 오는 여러 클라이언트의 요청을 처리해야하기 때문이다.

또한 다중 클라이언트 처리를 위해 쓰레드풀의 크기를 무한정 늘릴 수도 없다.

  • heap 을 많이 사용하면 GC 발생 주기는 길어지지만 그만큼 Stop-the-world 시간이 길어진다.
  • thread 가 많아진다는 건 그만큼 Context Switching 에 많은 리소스가 든다는 것이다.

JDK 1.4 부터 NIO 라는 논블로킹 I/O API 가 추가되었음
소켓도 입출력 채널의 하나로서 NIO API 를 이용한다

블로킹 소켓과 논블로킹 소켓은 데이터 송수신을 위한 함수의 동작방식에 따른 분류.
자바에서 논블로킹 소켓 : ServerSocketChannel, SocketChannel

Java NIO 컴포넌트 중 하나인 Selector 는 자신에게 등록된 채널에 변경사항이 생겼는지 검사하고 발생한 채널에 대한 접근을 가능하게 해준다.

  • ServerSocket 채널 객체를 Selector 채널에 등록하고 감지할 이벤트로 OP_ACCEPT 를 지정한다.

논블로킹 소켓에서의 read 메서드

클라이언트가 아직 전송하지 않았거나 데이터가 수신 버퍼까지 도달하지 않았다면, read 메서드는 0을 리턴한다.

비동기 호출이나 논블로킹 소켓을 사용하면 필연적으로 프로그램 복잡도가 증가한다. 하지만 네티는 개발자가 기능구현에 집중하도록 프레임워크 레벨에서 복잡한 로직을 모아 API 로 제공한다.

블로킹 소켓과 논블로킹 소켓의 동작방식 비교

소켓의 동작방식이 다르므로 입출력을 위한 메서드 및 프로그램 호출 구조가 다르다.
하지만 네티는 소켓의 모드와 상관없이 개발할 수 있도록 추상화된 전송 API를 제공한다. 따라서 소켓 모드를 바꾸기 위해서 데이터 송수신 부분의 로직을 고치지 않아도 된다.

이벤트 기반 프로그래밍

각 이벤트를 정의해두고 이벤트가 발생했을 때 실행될 코드를 준비해둔다.
논블로킹 소켓의 Selector 를 사용한 I/O 이벤트 감지 및 처리도 이벤트 기반 프로그램의 한 종류이다.

추상화 수준

얼마나 작은 단위로 이벤트를 나눌 것인지
이벤트 추상화가 너무 고수준이면 세부적 제어가 힘듦
너무 저수준이면 한 동작에 대해 너무 많은 이벤트가 발생하여 성능에 악영향

서버에 연결될 클라이언트 수는 매우 가변적이며 예측 불가하다.
이런 관점에서 서버에서 사용하는 이벤트 기반 프레임워크의 적절한 추상화 단위는 매우 중요하다.

이벤트 기반 네트워크 프로그래밍

이벤트를 발생시키는 객체와 발생될 이벤트 종류를 정의해야 한다.

네트워크 프로그램에서 이벤트 발생 주체는 '소켓' 이며, 이벤트 종류는 소켓연결, 데이터 송수신 이다.


소켓에 데이터를 직접 기록하고 읽으려면 소켓에 연결된 소켓채널(NIO) 또는 스트림을 사용해야 한다.


이벤트 핸들러 == 데이터 핸들러
네티는 데이터를 소켓으로 접근하기 위해 채널에 직접 쓰기/읽기 하지 않고 데이터 핸들러를 통한다.

  • 서버 코드를 클라이언트에서도 재사용 가능
  • 이벤트에 따라 로직 분리 -> 깔끔
  • 네트의 이벤트 핸들러는 에러 이벤트도 같이 정의 -> 에러처리 부담 낮아짐

3장. 부트스트랩

'부트스트랩'은 네티로 작성한 네트워크 프로그램이 시작할 때 가장 먼저 수행됨.
하는일
1. 어플리케이션이 수행할 동작을 지정
2. 프로그램에 대한 각종 설정을 지정한다.

부트스트랩에서 설정할 수 있는 것들에는 무엇무엇이 있을까?

  • 이벤트 루프
  • 채널 전송 모드
  • 채널 파이프라인

이벤트 루프에 대한 설정

  • 소켓채널에서 발생한 이벤트를 처리하는 스레드모델에 대한 구현이 담겨있음.

채널 전송 모드에 대한 설정

  • 블로킹/논블로킹/epoll

채널 파이프라인에 대한 설정

  • 소켓 채널로 수신된 데이터를 처리할 핸들러들을 지정한다.

예시) 네트워크에서 수신한 데이터를 단일 스레드로 DB 에 저장하는 프로그램 (8088 포트 사용, NIO 소켓모드 사용)

  • 채널 전송 모드 : 서버 소켓 채널 (NIO 소켓 모드) 사용
  • 채널 파이프 라인 : 데이터 핸들러(이벤트 핸들러) -> DB에 저장하는 로직
  • 8088 포트 바인딩
  • 이벤트 루프 : 단일 스레드를 지원하는 이벤트 루프 설정

부트스트랩의 구조

설정가능한 내용들

  • 전송 계층 (소켓 모드 및 I/O 종류)
  • 이벤트 루프 (단일, 다중 스레드)
  • 채널 파이프라인 설정
  • 소켓 주소와 포트
  • 소켓 옵션
  • 프로토콜

프로토콜은 채널 파이프라인에 등록되는 인코더/디코더 에서 처리한다

서버 어플리케이션을 위한 ServerBootstrap
클라이언트 어플리케이션을 위한 Bootstrap

  • ServerBootstrap 에 접속포트 지정하는 부분 제외하고는 두 클래스의 API 구조는 동일하다.

부트스트랩을 사용하면 네트워크 어플리케이션 작성시 유연성을 얻을 수 있다.
이전 2장에서 나온 BlockingServer 와 NonBlockingServer 예시에서, 만약 소켓 채널 입출력 방식이 변경되어야 한다면 소스코드 수정할 양이 무지막지하게 많다.

반면 네티를 사용한다면, 데이터 처리하는 코드를 변경하지 않고 부트스트랩의 설정만 변경해주면 된다. 이는 소켓 채널에 대한 입출력을 우아하게 추상화했기 때문에 가능하다. 사용할 입출력 모드에 해당하는 소켓 채널 클래스를 설정하기만 하면 변경이 완료된다.

ServerBootstrap API

group - 이벤트 루프 설정

클라이언트는 연결 요청 완료 후 데이터 송수신 처리를 위해 하나의 이벤트 루프로 모든 처리를 한다.

서버는 연결 요청 수락을 위한 루프 그룹, 데이터 송수신 처리를 위한 루프 그룹 2가지가 필요하다.

  • EventLoopGroup 중 부모 스레드 그룹 : 클라이언트의 연결을 수락하는 역할
  • EventLoopGroup 중 자식 스레드 그룹 : 클라이언트 소켓과 연결된 소켓의 '데이터 입출력' 및 '이벤트 처리'를 담당

channel - 소켓 입출력 모드 설정

부트스트랩 클래스를 통해 생성된 채널의 입출력 모드를 설정할 수 있다.
channel 메서드에 등록된 소켓 채널 생성 클래스가 소켓 채널을 생성한다.

설정가능한 클래스 목록

  • LocalServerChannel.class
  • OioServerSocketChannel.class
  • NioServerSocketChannel.class
  • EpollServerSocketChannel.class
    ..
    ..

channelFactory - 소켓 입출력 모드 설정

channel 메서드와 하는일은 동일하다.
네티가 제공하는 ChannelFactory 인터페이스의 구현체 : NioUdtProvider

handler - 서버 소켓 채널의 이벤트 핸들러 설정

서버 소켓 채널의 이벤트를 처리할 핸들러 설정
여기서 등록된 핸들러는 서버 소켓 채널에서 발생한 이벤트 만을 처리한다.
(데이터 송수신에 대한 이벤트는 여기서 등록된 핸들러에서 처리되지 않음 -> 클라이언트 소켓 채널에 대한 이벤트는 childHandler 에서 처리한다)

만약 여기에 LoggingHandler 를 추가한다면, 아래 로그들이 찍힌다.

  • 이벤트 루프에 등록, 포트에 바인드, 포트 활성화, 클라이언트 접속

childHandler - 클라이언트 소켓 채널의 데이터 가공 핸들러 설정

클라이언트 소켓 채널로 송수신되는 데이터를 가공하는 데이터 핸들러 설정 API
서버에 연결된 클라이언트 소켓 채널에서 발생하는 이벤트를 수신하여 처리한다.
서버 소켓 채널로 연결된 클라이언트 채널에 파이프라인을 설정하는 역할을 수행할 수 있다.

option - 서버 소켓 채널의 소켓 옵션 설정

소켓 옵션 == 소켓의 동작방식 지정
네티의 부트스트랩에서 설정할 수 있는 소켓 옵션 == 자바에서 설정할 수 있는 소켓 옵션

자바에서 socket.send 메서드가 호출되면 커널의 시스템 함수를 호출한다. 시스템 함수는 어플리케이션에서 수신한 데이터를 송신용 커널 버퍼에 쌓아두었다가 인터넷을 통해 전송한다.

기본적으로 부트스트랩의 소켓옵션은 커널에서 사용하는 소켓관련 설정값들을 변경하는 것이다. 즉 Netty 를 통해 생성된 소켓들은 이런 소켓 설정값들을 사용하겠다는 의미이다.

주요 소켓 옵션들

  • TCP_NODELAY
  • SO_KEEPALIVE
  • SO_SNDBUF, SO_RCVBUF
  • SO_REUSEADDR
    ..
    ..

childOption - 클라이언트 소켓 채널의 소켓 옵션 설정

서버에 접속한 클라이언트 소켓 채널에 대한 옵션을 설정

Bootstrap API

클라이언트 어플리케이션을 설정하는 Bootstrap 의 주요 API에 대한 설명
기본적으로 ServerBootstrap 과 같고 몇가지 측면에서 미세한 차이들을 같는다.
이는 필요할 때 살펴보면 되는 수준이다.

몇가지를 살펴보면, 클라이언트 에서 사용하는 단일 소켓 채널에 대한 설정이므로 부모 자식이라는 관계에 해당하는 API 들은 없다.

  • example) childHandler, childOption 같은 API들은 제공하지 않는다.

4장. 채널 파이프라인과 코덱

서버에 있는 채널은 2가지 종류

  • 서버 소켓 채널, 클라이언트 소켓 채널
  • 채널 == 소켓

채널 파이프라인

  • 채널에서 발생한 이벤트가 이동하는 통로

이벤트 핸들러

  • 채널 파이프라인을 통해 이동하는 이벤트를 처리하는 클래스

코덱

  • 이벤트 핸들러를 상속받아서 구현한 구현체들
  • 자주 사용하는 이벤트 핸들러들을 미리 구현해둔 코덱 묶음은 io.netty.handler.codec 패키지에 있다.
  • 대표적인 코덱으로는 HTTP 코덱이 있다.

이벤트 실행

네티는 네트워크 소켓에서 일어나는 여러가지 이벤트들을 '채널 파이프라인'과 '이벤트 핸들러' 로 추상화한다.

네티를 사용하면 데이터가 수신되었을 때의 메소드 호출이나 소켓의 연결이 끊겼을 때와 같은 예외처리 메소드 호출에 관여할 필요가 없다.

기본적인 네트워크 프로그램을 네티로 구현하기위해 해야할 것들

  1. 부트스트랩으로 네트워크 어플리케이션에 필요한 설정 지정
  2. 부트스트랩에 이벤트 핸들러들을 사용하여 채널 파이프라인을 구성
  3. 이벤트 핸들러의 데이터 수신 이벤트 메서드에서 데이터를 읽어들임
  4. 이벤트 핸들러의 네트워크 끊김 예외처리 메서드에서 에러 처리

네티의 이벤트 루프가 소켓 채널에서 발생한 이벤트를 처리하는 메서드를 자동으로 실행해준다.

데이터 수신 이벤트가 발생했을 때 이벤트루프가 이벤트를 처리하는 과정

  1. 네티의 이벤트 루프가 채널 파이프라인에 등록된 이벤트 핸들러를 가져옴
  2. 해당 핸들러에 데이터 수신 이벤트에 대한 메서드가 구현되어있다면 실행
  3. 해당 메서드가 없다면 다음 핸들러 가져옴
  4. 채널 파이프라인에 등록된 마지막 이벤트 핸들러까지 위 과정 반복

위와 같이 네티의 이벤트모델을 따르면 우리가 구현해야할 코드의 구분과 위치가 명확해지고 더 적은 코드로 튼튼한 네트워크 어플리케이션을 만들 수 있다.

채널 파이프라인

채널과 이벤트 핸들러 사이에서 연결 통로 역할을 수행한다.
이벤트 핸들러는 채널 파이프라인에 등록된다.
채널 파이프라인은 자신에게 등록된 이벤트 핸들러들의 (순서가 있는) 묶음이다.

채널에서 발생한 이벤트는 채널 파이프라인을 따라 흐른다.
흐르는 이벤트들을 수신하고 처리하는 기능은 이벤트 핸들러가 수행한다.
하나의 채널 파이프라인에 여러 이벤트 핸들러를 등록할 수 있다.

네티는 이벤트 처리를 위한 추상화 모델로서 채널 파이프라인을 사용한다.
소켓 채널에서 발생한 이벤트가 이를 통로로 하여 이동한다.

public class EchoServer {
    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
                        public void initChannel(SocketChannel ch) { // 클라이언트 소켓 채널이 생성될 때 실행됨
                            // 채널 파이프라인 설정
                            ChannelPipeline p = ch.pipeline();
                            p.addLast(new EchoServerHandler());
                        }
                    });

            ChannelFuture f = b.bind(8888).sync();

            f.channel().closeFuture().sync();
        }
        finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

채널 생성과 채널 파이프라인 구성

  1. 클라이언트 연결에 대응하는 소켓 채널 객체 생성, 빈 채널 파이프라인 객체 생성 후 채널에 할당
  2. 채널에 등록된 ChannelInitializer 인터페이스의 구현체를 가져와 initChannel 메서드 호출
  3. 1에서 등록한 빈 채널 파이프라인을 가져와 커스텀 이벤트 핸들러 객체를 파이프라인에 등록

이벤트 핸들러

소켓 채널에서 발생한 이벤트를 처리하는 인터페이스
채널 파이프라인에 등록되어 파이프라인으로 들어오는 이벤트를 이벤트루프가 가로채어 알맞은 메서드를 수행한다.

네티가 제공하는 이벤트의 종류, 발생시기(조건)를 잘 알아야한다.

네티는 비동기 호출을 지원하는 2가지 패턴을 제공함

  • 퓨처 패턴
  • 리액터 패턴

이벤트 핸들러는 리액터 패턴의 구현체이다.

TODO : 리액터 패턴이란?

채널 인바운드 이벤트

네티는 소켓 채널에서 발생하는 이벤트를 '인바운드 이벤트'와 '아웃바운드 이벤트'로 추상화한다.

인바운드 이벤트

  • 연결 상대방이 어떤 동작을 취했을 때 발생
  • 채널 활성화, 데이터 수신 등

  1. 클라이언트가 데이터 송신
  2. 네티가 소켓 채널에서 읽을 데이터가 있다는 이벤트를 채널 파이프라인으로 흘림
  3. 등록된 이벤트 핸들러 중, 인바운드 이벤트 핸들러가 해당하는 메서드 수행

네티는 인바운드 이벤트 핸들러를 ChannelInboundHandler 인터페이스로 제공한다.

channelRegistered 이벤트

채널이 이벤트루프에 등록되었을 때 발생

새로운 채널이 생성되는 시점에 발생

  • 서버에서는 서버 소켓 채널 생성시, 클라이언트 소켓 채널 생성시 발생
  • 클라이언트 에서는 connect 메서드를 수행할 때 (클라이언트 소켓 채널 생성시) 발생

channelActive 이벤트

channelRegistered 이후에 발생
채널이 생성되고 이벤트 루프에 등록된 후, 네티 API 를 이용하여 채널 입출력을 수행할 상태가 되었음을 알려주는 이벤트
서버 또는 클라이언트가 상대방에 연결한 직후에 한번 수행할 작업을 처리하기에 적합

channelRead 이벤트

데이터가 수신되었음을 알려주는 이벤트
수신된 데이터는 ByteBuf 객체에 저장되어 있음

channelReadComplete 이벤트

데이터 수신이 완료되었음을 알려주는 이벤트
채널의 데이터를 다 읽어서 더 이상 데이터가 없을 때 발생

channelInActive 이벤트

채널이 비활성화 되었을 때 발생
이 이후에는 채널에 대한 입출력 작업을 수행할 수 없음

channelUnRegistered 이벤트

채널이 이벤트 루프에서 제거되었을 때 발생
이 이후에는 채널에서 발생한 이벤트를 처리할 수 없음



채널 아웃바운드 이벤트

소켓 채널에서 발생한 이벤트 중 프로그래머가 요청한 동작에 해당하는 이벤트

  • 연결요청, 데이터 전송, 소켓 닫기
    채널 아웃바운드 이벤트를 처리하는 핸들러 구현을 위해 ChannelOutboundHandler 인터페이스를 사용한다.
    이 인터페이스의 핸들러 메소드들은 ChannelHandlerContext 객체를 인수로 받는다.
ChannelHandlerContext 의 역할
  1. 채널에 대한 입출력 처리
  • writeAndFlush 메서드로 채널에 데이터 기록
  • close 메서드로 채널의 연결을 종료시킴
  1. 채널 파이프라인에 대한 상호작용
  • 프로그래머에 의한 이벤트 발생
  • 채널 파이프라인에 등록된 이벤트 핸들러의 조회 및 동적 변경

ex) 데이터 수신 이벤트 처리 메서드에서 오류가 발생했고 오류처리 공통로직이 exceptionCaught 이벤트 메서드에 작성되어 있다면, fireExceptionCaught 메서드를 호출할 수 있음. 채널 파이프라인으로 exceptionCaught 이벤트 전달

bind 이벤트

서버 소켓채널이 클라이언트의 연결을 받아들이는 ip,port 정보 설정시 발생

connect 이벤트

클라이언트 소켓채널이 서버에 연결되었을 때 발생

disconnect 이벤트

클라이언트 소켓채널의 연결이 끊어졌을 때 발생

close 이벤트

클라이언트 소켓채널의 연결이 닫혔을 때

write 이벤트

소켓채널에 데이터가 기록되었을 때 발생

flush 이벤트

소켓채널에 대한 flush 메서드 호출시 발생


이벤트 이동 경로와 이벤트 메서드 실행

채널 파이프라인에 여러 개의 이벤트 핸들러가 등록되어 있을 때의 동작에 대해 알아보자

  • 등록된 이벤트 핸들러가 없다면 무시된다.
  • 등록된 이벤트 핸들러가 한 개이면 해당 핸들러가 적용된다.
  • 등록된 이벤트 핸들러가 여러 개이면 가장 앞에 있는게 적용되고 뒤로 전달되지 않는다.
    다음 이벤트 핸들러로 이벤트를 넘겨주는 방법은 channelHandlerContext.fireChannelRead 메서드 호출. 채널 파이프라인에 해당 이벤트를 발생시킨다.

코덱

네티에서 인코더는 전송할 데이터를 전송프로토콜에 맞춰 변환작업 수행하는 것
디코더는 반대작업 수행


코덱의 구조

수신 : 인바운드, ChannelInboundHandler == 디코더
송신 : 아웃바운드, ChannelOutboundHandler == 인코더
인코딩/디코딩은 어플리케이션 내부의 데이터를 각 프로토콜에 맞는 데이터로 변환하는 작업

ex) 네티가 제공하는 Base64Encoder를 채널 파이프라인에 등록되어 있다면, 채널에 데이터를 기록하면 ChannelOutboundHandlerAdapter의 write 이벤트 메서드가 수행된다.

기본 제공 코덱

네티는 자주 사용되는 프로토콜의 인코더, 디코더를 기본 제공한다.

  • HTTP 코덱, Base64 코덱, bytes 코덱, string 코덱, serialization 코덱, compression 코덱, protobuf 코덱, 등..

기본 제공 코덱들은 인바운드와 아웃바운드 핸들러를 모두 구현했다.
-> 네트워크 입출력 프로토콜을 구현한 것
ex) HttpServerCodec 생성자에서 HttpRequestDecoder, HttpResponseEncoder 를 모두 생성
HttpRequestDecoder : 수신된 ByteBuf 를 HttpRequest + HttpContent 로 변환

  • 수신한 이벤트와 데이터를 처리하여 HTTP 객체로 변환한 다음 channelRead 이벤트를 다음 이벤트 핸들러로 전달해 준다.
  • 수신된 HTTP 데이터에 대한 처리를 수행하는 데이터 핸들러를 '사용자가 구현'하여 붙일 수 있다 (웹 서버 만들 때 이런식으로 만든다)

HttpResponseEncoder : HttpResponse 객체를 ByteBuf 로 인코딩하여 송신

사용자 정의 코덱

사용자가 직접 필요한 프로토콜을 구현하는 것 -> 필요에 따라 인바운드와 아웃바운드 핸들러를 구현한다.

네티 기본 제공 코덱과 사용자정의 코덱을 함께 채널 파이프라인에 등록해서 쓸 수 있다.
채널 파이프라인에 등록된 이벤트 핸들러의 순서가 중요하다.

대부분의 어플리케이션 로직이 이벤트 핸들러에 구현된다.
네티가 제공하는 '코덱'은 이벤트 핸들러의 일종임을 잊지 말자.


네티의 이벤트 모델

이벤트 루프 : 이벤트를 실행하기 위한 무한루프 스레드

이벤트 큐에 이벤트를 등록하고 이벤트 루프가 큐에 접근하여 처리한다.
이벤트 루프가 다중 스레드이면 한 큐를 여러 스레드에서 공유해서 쓴다.

스레드 종류에 따른 구분

  • 단일 스레드 이벤트 루프
  • 다중 스레드 이벤트 루프

처리한 이벤트의 결과를 돌려주는 방식

  • 콜백 패턴
  • 퓨처 패턴

다중 스레드 이벤트 루프

장점

  • 다중 코어 CPU를 효율적으로 사용

단점

  • 이벤트 큐 하나를 공유해서 쓰기 때문에 동기화문제 발생 (스레드 경합)
  • 컨텍스트 스위칭 문제
  • 이벤트 발생 순서 ~ 처리 순서 불일치

네티의 다중 스레드 이벤트 루프

이벤트 발생순서에 따른 처리 순서를 보장한다.

어떻게?

  • 네티의 이벤트는 채널에서 발생한다
  • 각 이벤트 루프 스레드가 각자의 큐를 갖는다
  • 채널은 하나의 이벤트 루프에 등록된다

네티는 이벤트처리를 위해 SingleThreadEventExecutor 를 사용한다

네티의 비동기 I/O 처리

네티는 비동기 호출을 위한 두 가지 패턴을 제공
1. 리액터 패턴의 구현체인 이벤트 핸들러
2. 퓨처 패턴

네티의 퓨처 패턴

ChannelFuture는 채널 I/O의 비동기 호출을 지원하고자 제공된다.
비동기 I/O 메서드 호출 결과로 ChannelFuture를 돌려 받고 이걸 통해 작업 완료 유무를 확인한다.
네티에서는 ChannelFuture 객체에 작업이 왼료되었을 때 수행할 채널 리스너를 설정할 수 있다.

네티는 몇 가지 기본 채널 리스너를 제공하며 ChannelFutureListener 인터페이스를 통해 사용한다.

  • ChannelFutureListener.CLOSE : 작업 완료 이벤트 수신시 채널을 닫는다
  • ChannelFutureListener.CLOSE_ON_FAILURE : 작업 완료 이벤트 수신 & 결과가 실패일 때 채널을 닫는다
  • ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE : 작업 완료 이벤트 수신 & 결과가 실패일 때 채널 예외 이벤트를 발생시킨다

바이트 버퍼

자바가 버퍼 패키지를 제공함에도 네티는 내부에서의 데이터 이동, 입출력에 자체 버퍼 API 를 사용한다.

자바의 NIO 바이트 버퍼

바이트 데이터를 저장하고 읽는 저장소

배열을 멤버 변수를 갖고 있고 배열에 대한 읽고 쓰기를 추상화한 메서드 제공

  • 배열 인덱스 계산 없이 데이터 변경 처리 수행

저장되는 데이터형에 따른 바이트 버퍼 종류

  • ByteBuffer, CharBuffer, IntBuffer, ShortBuffer, LongBuffer, ..

바이트 버퍼 클래스는 내부의 배열 상태를 관리하는 3가지 속성을가진다

  • capacity : 버퍼에 저장가능한 데이터의 최대 크기. 불변 값.
  • position : 읽기/쓰기 작업 중인 위치
  • limit : 읽고 쓸 수 있는 버퍼 공간의 최대치

자바 바이트 버퍼 생성

allocate (힙 버퍼 생성)

  • JVM 힙 영역에 바이트 버퍼 생성
  • capacity 값 지정 가능
  • 값 0으로 초기화

allocateDirect (다이렉트 버퍼 생성)

  • 운영체제 커널 영역에 바이트 버퍼 생성
  • ByteBuffer 추상 클래스만 사용할 수 있음. 즉 다이렉트 버퍼는 ByteBuffer 로만 생성 가능

wrap

  • 입력된 바이트 배열을 사용하여 바이트 버퍼 생성

*다이렉트 버퍼는 힙 버퍼에 비해 생성시간 길지만 더 빠른 읽기 쓰기 성능 제공

자바 바이트 버퍼 사용법

바이트 버퍼를 사용할 때는 항상 버퍼 크기 (capacity, limit) 에 유의하며 사용해야 한다.

  • rewind() 메서드 : position 을 0 으로 초기화
  • flip() 메서드 : 이전에 수행한 put,get 메서드 호출 이후 마지막 작업 위치를 limit 속성으로 변경

flip 메서드

쓰기 작업 완료 이후에 데이터의 처음 부터 읽을 수 있도록 현재 포인터의 위치를 변경
읽기->쓰기 또는 쓰기->읽기 작업의 전환을 할 수 있게 해준다.

하나의 바이트 버퍼에 대해 읽기 또는 쓰기 작업의 완료를 의미하는 flip 메서드를 호출하지 않으면 반대 작업을 수행할 수 없다.


그림) 자바 바이트 버퍼에 데이터 기록 후 flip 메서드 호출한 상태

자바 바이트 버퍼 사용시 읽기/쓰기를 분리하여 생각해야 함.
다중 쓰레드 환경에서 바이트 버퍼를 공유하지 않아야 함.

네티는 이런 자바 바이트 버퍼의 문제점을 해결하기 위해 읽기 인덱스와 쓰기 인덱스를 별도로 제공한다.

네티의 바이트 버퍼

자바 바이트 버퍼에 비해 빠른 성능 제공
빈번한 바이트 버퍼 할당과 해제 부담 줄여 GC 부담 줄인다

네티 바이트 버퍼 특징

  • 별도 읽기 인덱스, 쓰기 인덱스 보유
  • flip() 메서드 없이 읽기 쓰기 작업 전환
  • 가변 바이트 버퍼
  • 바이트 버퍼 풀
  • 복합 버퍼
  • 자바 바이트 버퍼와 네티 바이트 버퍼 상호 변환

저장되는 데이터형에 따른 별도의 바이트 버퍼를 제공하지 않음
각 데이터형에 따른 읽기/쓰기 메서드 제공

  • readFloat, writeFloat 등

각 읽기/쓰기 메서드 실행시 각자의 인덱스 증가시킴

flip 같은 메서드 호출없이 읽기/쓰기 작업 번갈아 수행할 수 있음 (작업전환 용이)
하나의 바이트 버퍼에 대해 읽기/쓰기 작업 병행 가능

네티 바이트 버퍼 생성

프레임워크 레벨의 바이트 버퍼 풀 제공

  • 생성된 바이트 버퍼를 재사용

네티의 바이트 버퍼를 바이트 버퍼 풀에 할당 하기 위해

  • ByteBufAllocator 인테페이스 사용
  • PooledByteBufAllocator 추상 구현체 사용하여 각 바이트 버퍼 생성

네티 바이트 버퍼 생성시 고려할 것

  • 풀링 여부
  • 다이렉트 버퍼 여부

네티 바이트 버퍼의 종류

  • PooledHeapByteBuf
  • PooledDirectByteBuf
  • UnpooledHeapByteBuf
  • UnpooledDirectByteBuf

네티 바이트 버퍼 생성 방법

  • PooledByteBufAllocator.DEFAULT.headBuffer()
  • PooledByteBufAllocator.DEFAULT.directBuffer()
  • Unpooled.buffer()
  • Unpooled.directBuffer()

네티 바이트 버퍼 생성 메서드의 인수에 버퍼 크기를 지정하지 않으면 기본값인 256 바이트가 설정됨

버퍼 사용

네티 바이트 버퍼는 자바 바이트 버퍼와 다르게 읽기, 쓰기 인덱스를 따로 관리하기 때문에 서로간 작업전환이 자유롭다.

가변크기 버퍼

네티는 생성된 버퍼의 크기를 동적으로 변경할 수 있다
ex) buf.capacity(N)

바이트 버퍼 풀링

프레임워크에서 바이트 버퍼 풀을 제공

  • 다이렉트 버퍼와 힙버퍼 모두 풀링 가능

장점

  • 버퍼 할당/해제시 발생하는 GC 횟수 감소

풀링은 ByteBufAllocator 사용하여 바이트 버퍼 생성시 자동으로 수행됨

네티는 바이트 버퍼 풀링을 위해 바이트 버퍼에 참조 수를 기록함
참조수 관리를 위해 ReferenceCountUtil 클래스의 retain, release 메서드 사용

  • retain : 참조 수 증가
  • release : 참조 수 감소

엔디안 변환

네티 바이트 버퍼의 기본 엔디안은 자바와 동일하게 빅 엔디안 이다.

자바 바이트 버퍼와의 상호호환

nioBuffer 메서드를 사용하여 자바 NIO 버퍼로 변환 가능. 반대도 가능.
-> 변환된 NIO 바이트 버퍼는 네티 바이트 버퍼의 내부 바이트 배열을 공유

채널과 바이트 버퍼 풀

네티 내부에서 데이터 처리시 네티의 바이트 버퍼를 사용함

  • channelRead 메서드 실행된 후 네티 바이트 버퍼는 바이트 버퍼 풀로 돌아간다.

네티 바이트 버퍼 풀은 네티 어플리케이션의 서버 소켓 채널이 초기화될 때 같이 초기화되며 ChannelHandlerContext 인터페이스의 alloc 메서드로 생성된 바이트 버퍼 풀을 참조할 수 있다.

// 바이트 버퍼 풀을 관리하는 인터페이스
ByteBufAllocator bytebufAllocator = channelHandlerContext.alloc();

// ByteBufAllocator 의 풀에서 관리되는 바이트 버퍼. 사용 다되면 다시 풀로 돌아감.
ByteBuf newBuffer = bytebufAllocator.buffer();

// ### newBuffer 사용

// write 메서드 인수로 사용된 바이트 버퍼는 데이터를 채널에 기록하고 난 뒤 버퍼 풀로 돌아감.
channelHandlerContext.write(msg);

네티 바이트 버퍼 장점 다시 한번 요약

  • 자바 바이트 버퍼의 flip() 메서드 같은거 호출 안해도 됨 (write/read index 따로 관리)
  • 바이트 버퍼 풀 -> GC 빈도 줄여줌

네티가 제공하는 바이트 버퍼 풀이 언제 생성되고 어떻게 유지되는지 이해하는 것이 메모리의 효율적 사용 측면에서 매우 중요하다.

  • 바이트 버퍼 풀 : 서버 소켓 채널이 초기화될 때 같이 초기화
profile
momentum

1개의 댓글

comment-user-thumbnail
2024년 4월 14일

글 너무 잘읽었습니다. 감사합니다.

답글 달기