Netty 아키텍처 기초 소개 및 사용법
소개
Netty는 Java를 통해 소켓 통신 + 비동기 프로그래밍을 고성능으로 단순하게 구현할 수 있는 기반이 튼튼한 프레임워크다.
국내에서도 거래가 여러번 일어나는 프로젝트 뿐 아니라 WebFlux 기반의 고성능 서버를 구축할 때도 많이 사용 중이다.
기존에는 HTTP 통신을 위해서 HttpURLConnection를 사용하고 TCP 소켓 통신을 위해서는 java.net의 ServerSocket를 이용했다.
다만 해당 방식은 동기 전송 방식만 지원했는데 해당 방식을 Blocking IO, 즉 OIO(Old Input Output)라고 불렀다.
그리고 2002년인 자바 4 버전부터 None Blocking IO 패키지가 포함된 java.nio인 NIO(New Input Output)가 출시되었다.
다만, NIO가 생겼다고 하더라도 비동기 프로그래밍은 순수 자바 라이브러리를 직접 사용해 애플리케이션을 제작하기가 아주 어려웠다. 이 NIO를 편하게 개발하도록 도와주는 것이 Netty다.
Netty는 Server로서도 활용 될 수도 있고 라이브러리로도 사용할 수 있다.
Spring Web Flux에서는 기본 서버를 Netty로 사용한다.
물론 Java의 NIO 방식을 이용한 Tomcat을 사용할 수도 있다.
기존 MVC의 성능에 크게 구애받지 않고 지속적 비동기 요청 시스템을 만들고 싶은 경우에도 Netty 라이브러리를 사용할 수 있다.
결과적으로 Netty는 비동기, 소켓, 고성능 프로그래밍을 하기 위해서 항상 좋은 선택이 될 수 있다.
코드 형식
EventLoopGroup parentGroup = new NioEventLoopGroup(1);
EventLoopGroup childGroup = new NioEventLoopGroup();
try{
ServerBootstrap b = new ServerBootstrap();
b.group(parentGroup, childGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
ChannelPipeline cp = sc.pipeline();
cp.addLast(myHandler);
}
});
ChannelFuture cf = b.bind(8080).sync();
cf.channel().closeFuture().sync();
}catch(Exception e){
e.printStackTrace();
}
finally{
parentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
BootStrap
Netty의 시작이 되는 코드는 Bootstrap에서부터 시작한다.
대표적인 종류는 ServerBootStrap, BootStrap 2가지가 있다.
ServerBootStrap은 Server용으로 쓰이고 BootStrap은 Client용으로 쓰인다.
ServerBootStrap은 Client의 기능을 포함하고 있다.
ServerBootStrap은 클라이언트의 요청을 수락하기 위해(예를 들어 서로 서버를 통해 채팅을 하는 프로그램을 만드는 경우) 구현될 수 있다. 서버의 기능이 필요하지 않다면 Client 기능만을 이용한 BootStrap으로 구현하면 된다.
EventLoopGroup
Netty는 Channel에서 발생하는 이벤트들을 Event Loop가 처리하는 구조다.
Channel에서 발생한 이벤트는 이벤트 큐에 쌓이게 되는데 쌓인 이벤트들을 Event Loop가 Event Driven 방식에 따라 처리한다.
ServerBootStrap은 Server에서 발생하는 Event Loop 그룹 1개와, Client에서 발생하는 Event Loop 그룹 1개를 필수로 등록해줘야한다.
Bootstrap은 Client에서 발생하는 Event Loop 그룹 1개를 필수로 등록한다.
Channel
소켓이 이용할 수 있는 Channel은 Handler를 통해 발생한 실제 이벤트를 전송해주는 운송수단이라고 생각하면 된다.
보통 Server는 NioServerSocketChannel.class, Client는 NioSocketChannel.class를 이용한다.
Handler를 통해 받아온 Channel은 실제 연결이 된 대상 서버를 가리키므로
상대방에게 writeAndFlush 메소드를 통해 메시지를 전달할 수 있다.
Handler
handler는 Netty Client를 작성할 때 사용된다.
childHandler는 Netty Server를 작성할 때 보통 사용된다.
ServerBootStrap에 위처럼 handler(new LoggingHandler~를 추가하면 서버에서 발생한 로그를 확인할 수 있다.
실제 이벤트(비즈니스 로직)가 발생되는 클래스를 등록하는 곳이다.
Pipeline으로 순서 처리를 보장할 수 있다.
ChannelPipeline를 가져와서 순서대로 처리할 Handler를 등록할 수 있다.
실제 연결
Netty는 Callback 방식과 Future 방식 둘 다 지원한다.
위에서 만든 bootstrap을 bind()나 connect()로 연결해서 sync() 메소드를 이용해 동기화(Blocking)한다.
그럼 연결이 될 때까지 대기를 하게 되는데 받는 클래스를 Channel로 하면 Callback 방식이 되고 ChannelFuture로 하면 Future 방식으로 이용할 수 있다.
ChannelFuture는 채널이 종료될 때 알려줄 수 있도록 위처럼 동기화를 해서 대기 시켜놓을 수 있는 기능이 있다.
그래서 Future는 결과를 얻기 위해서는 반드시 블로킹해야만 한다.
closeFuture().sync()는 서버 소켓 종료를 의미에 대한 "미래 시점"을 제공하고 sync가 완료될 때까지 기다린다.
이렇게 되면 Spring Rest Api를 사용할 수도 없을 수 있으니 아래 글을 참조하자.
@PreDestroy를 사용하면 Future, 즉, 미래시점이 반납될 때까지 동기화하지 않고 사용할 수 있다.
그 외(Option)
.option(ChannelOption.SO_KEEPALIVE, true) //상대방의 상태를 확인하는 패킷을 전송
등 Netty 설정을 할 수 있다.