본문 바로가기
Old Posts/Java

[Java] Netty 프레임워크 소개

by A6K 2021. 5. 14.

Netty 프레임워크 소개

Netty 프레임워크는 전세계 개발자들이 사용하는 자바 네트워크 애플리케이션 프레임워크다. Netty의 공식 홈페이지(netty.io)에서는 'Netty는 비동기 이벤트 기반 네트워크 응용프로그램 프레임워크입니다'라고 소개하고 있다.

Netty는 아파치 프로젝트의 다양한 오픈소스들은 물론 카카오와 라인, 네이버는 물론 애플, 트위터 등에서도 사용되고 있다. 한국 개발자인 이희승님이 창시자로 알려져있다. (관련자료 : 비동기를 사랑하는 오픈소스 개발자, 이희승 - LINE Engineering)

네티 이전에도 이런 프레임워크들이 있었겠지만 대부분 자바 네트워크 프로그래밍은 Java NIO를 이용해 진행했다. 물론 Java NIO 역시 강력하지만 순수 NIO만을 이용해서 네트워크 애플리케이션을 작성하는 일은 매우 어렵고 비효율적이다. 때문에 예상치 못한 버그를 만들어 낼 수 있다.

네티는 단순히 네트워크 통신과 관련된 기능을 제공할 뿐만 아니라 일반적으로 네트워크 애플리케이션에서 사용하는 다양한 기능들을 포함하고 있다. 덕분에 자바 프로그래머들은 네트워크 프로그래밍이나 멀티스레드와 관련된 처리보다는 자신들의 비즈니스 로직에 좀 더 집중할 수 있게되었다. 네티를 이용한 애플리케이션은 최소 10만개 이상의 클라이언트 커넥션을 처리할 수 있을 정도로 안정되어 있다.

Netty는 자바 개발자가 네트워크 애플리케이션을 빠르고 쉽게 개발할 수 있도록 다양하고 강력한 기능들을 제공한다.

Netty 프레임워크 특징

Asynchronous IO (비동기 입출력)

우선 Netty 프레임워크는 비동기처리를 지향한다. 비동기라하면 요청을 보낸 후 즉시 리턴된 다음, 다른 작업을 하다가 요청한 작업의 처리가 완료되었는지 확인한 다음 나중에 응답을 받는 방식이다. 요청을 보내고 요청이 끝날 때까지 기다리는 동기방식과 대비되는 개념이다.

Event Model

Netty는 입출력을 위해 잘 정의된(Well-defined) 이벤트 모델을 가지고 있다.

사용자가 코어 로직을 손대지 않고도 직접 이벤트 타입을 구현할 수 있도록 지원한다. 각 이벤트 타입은 엄격하게 계층화되어 있어 서로 잘 구분된다. 다른 NIO 프레임워크들은 이벤트 타입을 추가할 경우 기존 코드에 영향을 주거나 아예 커스텀 이벤트 추가를 막는 경우도 많다.

ChannelEvent는 ChannelHandlers에 의해 ChannelPipeline에서 처리된다. 파이프라인은 Intercepting Filter 패턴을 구현한다. 사용자는 이벤트가 어떻게 처리되는지 어떻게 파이프라인의 핸들러들이 서로 상호작용하는지에 대해 컨트롤을 할 수 있다.

Universal Asynchoronous I/O API

자바의 전통적인 IO API는 TCP, UDP에 따라 다른 형태의 API를 제공한다. 이 때문에 TCP 애플리케이션을 UDP로 포팅하는게 마냥 쉽지만은 않다. 심지어 NIO라는 독특한 형태의 API가 나왔으며 NIO2까지 나왔다. 각각은 다 특성과 성능이 다르기 때문에 자바 개발자가 어떤 것을 이용해서 개발을 해야하는지 미리 결정해야하는 경우가 종종 있다.

예를 들어 서비스 초창기에 시스템 규모가 작을 때에는 Old IO를 사용해서 쉽게 구현했던 서비스가 시스템이 성장하면서 다양한 문제에 직면할 가능성이 있다. 그렇다고 NIO로 먼저 구현한다고 생각해보면 복잡성 때문에 빠른 개발이 불가능할 수 있다.

Netty는 Channel이라는 Async I/O 인터페이스를 가지고 있다. 이 채널은 Point-to-Point 통신을 추상화한 개념이다. 일단 Netty앱을 개발하면 추상화를 통해서 실제 통신 부분과 상관없이 로직을 개발할 수 있다. Netty는 여러개의 필수적인 transports를 하나의 API를 통해서 제공한다.

  • NIO 기반의 TCP/IP
  • Old IO 기반의 TCP/IP
  • Old IO 기반의 UDP/IP
  • Local transport

이 transport 사이를 전환하는 것은 팩토리 클래스인 ChannelFactory를 고르는 몇 줄만 고치면 된다. 심지어는 아직 구현되지 않은 transport를 사용하는 클라이언트 코드를 미리 구현할 수도 있다. transport 역시 직접 구현해서 사용할 수 있다.

ChannelBuffer

Netty는 NIO의 ByteBuffer를 사용하지 않고 독자적인 Buffer API를 구현해서 사용했다. ByteBuffer는 사용해본 개발자는 알겠지만 flip()을 중간중간에 호출하면서 사용해야되므로 매우 헷갈린다. 쓰기 어렵고 헷갈린다는 것은 곳 버그 발생 확률이 높다는 뜻이다. 또, ByteBuffer를 사용하는 경우 GC가 많이 발생할 수도 있다.

Netty는 이런 ByteBuffer 대신 독자적인 버퍼를 구현했다. 독자적인 버퍼를 구현해서 제로카피(Zero Copy)도 지원하고, ByteBuffer보다 빠른 성능을 구현했다. 다이나믹 버퍼 타입도 지원한다.

그 밖에

그 밖에 빠른 개발을 위한 수 많은 컴포넌트들을 가지고 있다.

네트워크 프로그래밍에서는 비즈니스 로직과 프로토콜 코덱을 분리하는게 좋다. Netty는 이미 수 많은 코덱을 제공하고 있다.

NIO에서 SSL 지원은 일반적이지 않았다. 하지만 Netty에서는 SSL도 구현했으며, 사용자는 그냥 가져다 쓰면 된다. Netty는 HTTP도 구현했다. Protocol Buffer도 구현했다..

뭐 있으면 좋은 것들은 다 구현되었거나 구현되고 있다. 프레임워크 사용자는 그냥 가져다가 비즈니스 로직에 조립하기만 하면 된다. (자세한 것들은 관련 키워드로 검색하면 예제 코드부터 많이 나온다)

Netty 사용 (Maven)

메이븐 프로젝트에서 네티(Netty)를 사용하기 위해서는 pom.xml 파일에 다음 의존성(dependency)을 추가하면 된다.

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.60.Final</version>
    <scope>compile</scope>
</dependency>

상세한 버전은 잘 골라서 사용하면 된다.

Netty와 관련된 개념들

Netty를 잘 사용하기 위해서 그리고 Netty 문서와 커뮤니티에서 오가는 내용들을 이해하기 위해서 우선 알아둬야할 핵심 개념들이 몇 가지 있다.

Netty가 실행되는 순서는 다음과 같다.

각각에 대해서 알아보자.

Bootstrap

Bootstrap 클래스는 Netty의 스레드를 생성하고, 소켓을 오픈하는 등 Netty를 구동하기 위한 부트스트래핑을 위해 사용하는 클래스다.

EventLoopGroup

Netty의 EventLoopGroup은 EventLoop들의 그룹이다. 여러개의 EventLoop은 하나의 그룹으로 모아질 수 있다. 같은 그룹에 속한 EventLoop들은 스레드와 같은 몇몇 리소스들을 공유하게 된다.

EventLoop

Netty의 EventLoop은 새로운 이벤트를 반복적으로 확인하는 루프다. 예를들어 SocketChannel로부터 새로운 데이터가 들어오는 것 같은 이벤트를 매번 확인한다. 이벤트가 발생하면 적당한 이벤트 핸들러에 전달된다.

SocketChannel

Netty의 SocketChannel은 TCP 연결을 대표한다. 네트워크 프로그램에서 Server나 Client가 Netty를 사용한다면, 머신 사이에서 데이터를 전달하는 과정은 SocketChannel을 통해서 이뤄진다.

SocketChannel은 항상 같은 EventLoop에 의해 관리가 된다. 같은 EventLoop은 항상 같은 스레드에서 실행이 되기 때문에 SocketChannel은 항상 같은 스레드에서 접근된다. 이 때문에 순서가 보장된다. 따라서 같은 SocketChannel에서 동시에 데이터가 읽히는 것에 대해서는 걱정하지 않아도된다.

ChannelInitializer

Netty의 ChannelInitializer는 SocketChannel이 생성될 때 ChannelPipeline에 추가되는 특별한 ChannelHandler다. 이 객체는 SocketChannel을 초기화하는 역할을 한다.

SocketChannel이 초기화되면 ChannelPipeline에서 ChannelInitializer가 제거된다.

ChannelPipeline

각각의 SocketChannel은 ChannelPipeline을 가지고 있다. 채널파이프라인은 ChannelHandler 인스턴스의 리스트다. EventLoop이 데이터를 SocketChannel에서 읽으면 데이터는 파이프라인에 있는 첫번째 채널핸들러에게 넘겨진다. 첫 번째 핸들러는 넘겨받은 데이터를 처리하고, 필요한 경우 파이프라인의 다음 핸들러로 데이터를 넘길 수 있다.

데이터를 SocketChannel로 쓰는 경우도 마찬가지로 ChannelPipeline을 타게 되며, 핸들러들을 거친다음 SocketChannel로 쓰여지게 된다.

ChannelHandler

ChannelHandler는 SocketChannel에서 읽혀진 데이터나 SocketChannel로 쓰여질 데이터를 다루게 된다.

Netty - ChannelPipeline

채널 파이프라인은 Netty 애플리케이션의 핵심이다. 각 TCP 연결에 해당하는 SocketChannel은 ChannelPipeline을 가지고 있다. 채널 파이프라인은 ChannelHandler 인스턴스의 리스트다. 각 ChannelHandler 인스턴스들은 SocketChannel 쪽으로 데이터를 넘기거나, SocketChannel 쪽에서 데이터를 얻어온다.

ChannelHandler는 두가지 서브 인터페이스가 있다.

  • ChannelInboundHandler
  • ChannelOutboundHandler

소켓을 통해서 SocketChannel에 들어온 데이터는 ChannelInboundHandler 체인을 거치면서 애플리케이션으로 넘어온다. 반대로 애플리케이션이 소켓을 통해 전송하려는 데이터는 ChannelOutboundHandler 체인을 거치면서 처리된 후 소켓을 넘어간다.

이 체인을 이용해서 애플리케이션이 네트워크 통신을 할 때 적당한 처리를 먼저 할 수 있다. 예를 들어 네트워크를 통해 객체를 전송하는 경우, 객체를 Netty로 넘겨주면 Channel 핸들러 체인 안쪽에서 객체를 직렬화(Serialize)하고, 필요한 경우 압축하고, 소켓을 통해 좀 더 작은 단위로 바이트 배열을 전송하는 과정을 거치게 할 수 있다. 이런 과정은 한번 Netty 프레임워크에 구현해 놓으면, 애플리케이션 개발자는 그냥 객체를 write() 하는 동작만으로 전송이 가능하다.

위 그림에서는 두 개의 체인이 있는 것처럼 보이지만 사실은 하나의 리스트로 묶여 있고, Inbound와 outbound로 구분되는 형태로 되어 있다.

경우에 따라서는 ChannelInboundHandler에서 넘어온 데이터를 처리하다가 Handler에서 SocketChannel로 다시 데이터를 써서 OutboundHandler를 타고 다시 전송되도록 할 수도 있다. 뭐 네트워크에서 두 노드간 Backpressure 같은 것을 이런 식으로 구현하면 편할 것 같다.

Netty는 이런 식으로 동작한다. 이제 Netty 애플리케이션 코드를 보면서 어렵지 않게 이해할 수 있을 것이다.

댓글