코딩하는 오징어
Netty의 기본 Component 및 Architecture 본문
Netty는 네트워크적인 low한 처리와 비즈니스 로직 처리를 추상화를 통해 분리하였으며 덕분에 비즈니스 로직에 더욱 집중할 수 있도록 도와준다. Netty는 크게 다음과 같은 Component들을 통해 데이터를 처리한다.
- Channel, EventLoop, ChannelFuture
- ChannelHandler, ChannelPipeline
- Bootstrap
Channel
기본 입출력 작업(bind, connect, read, write)은 네트워크 전송에서 제공하는 기본형을 이용한다. 자바 기반 네트워크 기본 구조는 Socket클래스이다. Netty의 Channel 인터페이스는 Socket으로 직접 작업할 때의 복잡성을 크게 완화하는 API를 제공한다. Netty는 Channel 인터페이스를 구현한 몇 가지 특수한 구현체를 제공한다.
- EmbeddedChannel
- LocalServerChannel
- NioDatagramChannel
- NioSctpChannel
- NioSocketChannel
EventLoop
EventLoop는 연결의 수명주기 중 발생하는 이벤트를 처리하는 Netty의 핵심 추상화를 정의한다. Netty의 스레드 처리 모델은 내용이 많으므로 별도의 글을 통해 설명하겠다. 다음은 EventLoop, Thread, EventLoopGroup 간의 관계를 개략적으로 보여준다.
이 설계에서는 한 Channel의 입출력이 동일한 Thread에서 처리되므로 동기화가 필요없다.
ChannelFuture
Netty의 모든 입출력 작업은 비동기적이다. 이를 위해 Netty는 ChannelFuture를 제공하며, 이 인터페이스의 addListener() 메서드는 작업이 완료되면(성공 여부와 관계없이) 알림을 받을 ChannelFutureListener 하나를 등록한다.
ChannelHandler
Application을 개발하는 관점에서 Netty의 핵심 Component는 인바운드와 아웃바운드 데이터의 처리에 적용되는 모든 Application 논리의 컨테이너 역할을 하는 ChannelHandler이다. ChannelHandler의 메서드가 네트워크 이벤트에 의해 트리거 되기 때문이다. 실제로 ChannelHandler는 데이터를 다른 포맷으로 변환하거나 작업 중 발생한 예외를 처리하는 등 거의 모든 종류의 작업에 활용할 수 있다. 그 예로, 우리가 자주 구현하는 ChannelHandler의 하위 인터페이스인 ChannelInboundInterface가 있다. 이 인터페이스는 Application으로 들어오는 데이터에 의해 트리거되어 메서드가 호출된다.
ChannelPipeline
ChannelPipeline은 ChannelHandler 체인을 위한 컨테이너를 제공하며, 체인 상에서 인바운드와 아웃바운드 이벤트를 전파하는 API를 정의한다. Channel이 생성되면 여기에 자동으로 자체적인 ChannelPipeline이 할당된다. ChannelHandler는 다음과 같이 ChannelPipeline안에 설치된다.
- ChannelInitializer 구현체를 ServerBootstrap에 등록한다.
- ChannelInitializer.initChannel()이 호출되면 ChannelInitializer가 ChannelHandler의 커스텀 집합을 파이프라인에 설치한다.
- ChannelInitializer는 ChannelPipeline에서 자신을 제거한다.
ChannelHandler는 광범위한 용도를 지원할 수 있게 설계되었으며, ChannelPipeline을 통해 오가는 이벤트(데이터 포함)를 처리하는 모든 코드를 위한 범용 컨테이너라고 할 수 있다. 핵심 하위 인터페이스로는 ChannelInboundHandler와 ChannelOutboundHandler가 있다. 파이프라인을 통해 이벤트를 이동하는 역할은 Application의 부트스트랩 단계나 초기화 중에 설치된 ChannelHandler가 담당한다. 이들 객체는 이벤트를 수신하고, 구현된 처리 논리를 실행하며, 체인 상의 다음 ChannelHandler로 데이터를 전달한다. 실행되는 순서는 추가된 순서에 의해 결정된다. ChannelPipeline이라고 말할 때는 이러한 ChannelHandler의 정렬된 배치 전체를 의미한다고 보면 된다.
아웃바운드와 인바운드 작업이 서로 다르다는 것을 감안할 때, 동일한 ChannelPipeline 안에 두 가지 핸들러 범주가 혼합돼 있으면 Netty는 ChannelInboundHandler와 ChannelOutboundHandler의 구현을 구분하며, 핸들러 간의 데이터 전달이 동일한 방향으로 수행되도록 보장한다.
ChannelHandler를 ChannelPipeline에 추가할 때 ChannelHandler 및 ChannelPipeline 간의 바인딩을 나타내는 ChannelHandlerContext 하나가 할당된다. 이 객체는 기본 Channel을 가져오는 데 이용할 수 있지만, 실제로는 아웃바운드 데이터를 기록할 때 주로 이용된다.
Netty에서 메시지를 보내는 데는 Channel에 직접 기록하거나 ChannelHandler와 연결된 ChannelHandlerContext 객체에 기록하는 두 가지 방법이 있다. 전자는 ChannelPipeline에 끝단에서 시작되며, 후자는 메시지를 기록한 핸들러의 다음 핸들러에서 시작된다.
인코더와 디코더도 ChannelHandler 인터페이스의 구현체이며 Binary 데이터를 Application에서 사용할 포맷에 맞게 변환하거나 Application의 포맷을 Binary 데이터로 변환하여 출력하는 로직에 사용된다.
Bootstrap
Netty의 부트스트랩 클래스는 프로세스를 지정된 포트로 바인딩하거나 프로세스를 지정된 호스트의 지정된 포트에서 실행 중인 다른 호스트로 연결하는 등의 일을 하는 Application의 네트워크 레이어를 구성하는 컨테이너를 제공한다. 부트스트랩에는 클라이언트용과 서버용의 두 가지 유형이 있으며 다음과 같다.
- Bootstrap (클라이언트용)
- ServerBootstrap (서버용)
위의 설명을 토대로 EchoServer & EchoClient 샘플 코드를 본다면 좀 더 이해가 잘 될것으로 생각된다.
/*
* EchoServer 코드
*/
public class EchoServer {
private final int port;
public EchoServer(port) {
this.port = port;
}
public void start() throws Exception {
EchoServerHandler handler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.localAddress(new InetSocketAddress(this.port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(handler); // ChannelHandler 등록
}
});
ChannelFuture future = bootstrap.bind().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println(">>> Server Received : " + in.toString(CharsetUtil.UTF_8));
String toUpper = in.toString(CharsetUtil.UTF_8).toUpperCase();
ctx.write(Unpooled.copiedBuffer(toUpper.getBytes()));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(">>> Echo Server Error!!!!");
cause.printStackTrace();
ctx.close();
}
}
/*
* EchoClient 코드
*/
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public void call() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(this.host, this.port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} finally {
group.shutdownGracefully().sync();
}
}
}
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty World!!", CharsetUtil.UTF_8));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
System.out.println(">>> Client Received : " + msg.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(">>> Echo Client Error!!!!");
cause.printStackTrace();
ctx.close();
}
}
/*
* EchoServer와 EchoClient 실행 코드
*/
public class EchoServerExample {
public static void main(String... args) {
EchoServer server = new EchoServer(19000);
try {
server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class EchoServerTest {
@Test
void call() throws Exception {
EchoClient client = new EchoClient("localhost", 19000);
client.call();
}
}
EchoServer를 실행시킨 후 테스트 코드를 실행시켜 EchoClient를 통해 문자열을 보내면 Server에서 대문자로 변환하여 돌려준다.
EchoServer 출력
EchoClient 출력
'Framework > Netty' 카테고리의 다른 글
Reference Count를 통한 Netty의 ByteBuf memory 관리 (1) | 2023.04.15 |
---|---|
Netty의 스레드 모델 (4) | 2020.06.14 |