티스토리 뷰

평소에 Netty(Netty 이전에는 MINA)에 관심도 많았고, 또 현재 진행 중인 프로젝트에도 Netty 3.x 를 사용 중인지라 Netty 4.0 에 대해 어떤 부분들이 변경되었는지 늘 궁금하던 차에.. 이참에 문서도 볼겸해서 이렇게 발번역(?)을 하게 되었다. 영어울렁증이 심한 나로썬 쉽지 않긴 했지만, 나름 진행하면서 Netty 4.0에 대해서 대략적인(?) 내용을 알 수 있는 계기가 되어 좋았다. 물론 번역에 대한 퀄리티는 절대(!!) 보장할 수 없으니 혹시라도 이 글을 보게 된다면 반드시 원문도 함께 보아야 할 것이다~~~~

번역하다 도저히 이해가 안되거나 하는 부분에 대해서는 'wiz talk' 으로 시작하는 코멘트를 달거나 아니면 그냥 영어원문을 쓰기도 했음을 참고해 주길 바란다.(제발 해치지 말아 주시길..ㅜㅜ)

(참고로 [MINA에 대한 글]도 예전에 한번 쓰긴 했는데 뒷심부족으로 더이상 진도가 안나갔다. ㅜㅜ;)


[원문] 
http://netty.io/wiki/new-and-noteworthy.html


새롭고 주목할 만한 것들

이 문서는 기존 어플리케이션을 새로운 버전으로 변환하기 위해 아이디어를 당신에게 제공하기 위해 나온 Netty4에서 주목할 만한 변화와 새 기능들의 리스트를 보여준다.


프로젝트 구조 변화

Netty의 패키지 이름이 org.jboss.netty 에서 io.netty 로 변경되었다.(더 이상 JBoss.org의 일원이 아니기 때문에..)

바이너리 Jar파일은 여러 개의 서브모듈로 분리되어서 사용자는 불필요한 기능들에 대한 클래스 패스를 제외시킬 수 있다. 현재 구조는 아래와 같다.

Artifact IDDescription
netty-parentMaven parent POM
netty-commonUtility classes and logging facade
netty-bufferByteBuf API that replaces java.nio.ByteBuffer
netty-transportChannel API and core transports
netty-transport-rxtxRxtx transport
netty-transport-sctpSCTP transport
netty-transport-udtUDT transport
netty-handlerUseful ChannelHandler implementations
netty-codecCodec framework that helps write an encoder and a decoder 
netty-codec-httpCodecs related with HTTP, Web Sockets, SPDY, and RTSP
netty-codec-socksCodecs related with SOCKS protocol
netty-allAll-in-one JAR that combines all artifacts above
netty-tarballTarball distribution
netty-exampleExamples
netty-testsuite-*A collection of integration tests
netty-microbenchMicrobenchmarks 


* wiz talk ======================================
내가 보기에 눈에 띄게 바뀐 부분은 대략 아래 정도~
java.nio.ByteBuffer가 ByteBuf로 변경.
새로운 채널(Rxtx, SCTP, UDT) 추가(이에 따른 코덱도 추가)
기존 코덱에서 Frame, Relay, OneOne 가 삭제.(삭제된 다른 코덱들도 있으나 일단 이 3개는 나도 줄기차게 썼던 코덱이라 왠지 애정이 간다)
io.netty.util.concurrent 패키지 추가.
==============================================


모든 artifact(netty-all.jar 제외)는 OSGi 구성으로 되어 있으며, 당신이 원하는 OSGi 컨테이너에서 사용할 수 있다.
OSGi 소개 : http://ko.wikipedia.org/wiki/OSGi


일반적인 API 변화
* Netty에서의 모든 동작은 간결함을 위해 method-chaning을 지원한다.
* Non-Configuration getters 는 더이상 'get-' prefix를 갖지 않는다.( 예: Channel.getRemoteAddress() → Channel.remoteAddress())
 - 단, boolean 속성은 혼란을 피하기 위해 'is-' prefix를 유지한다.(예: 'empty'는 관사이자 동사이기 때문에 'empty()'는 두가지 의미를 가질 수 있다.)
* 4.0 CR4와 CR5간에 API 변화를 알기 위해서는 Netty 4.0.0.CR5 released with new-new API 를 보면 된다.


Buffer API 변화
ChannelBuffer -> ByteBuf
위에서 언급한 구조의 변화처럼 고맙게도 Buffer API는 분리된 패키지로 사용될 수 있다. 당신이 네트워크 어플리케이션 프레임워크로서 Netty를 적용하는 것에 관심이 없을지라도 Buffer API만 당신이 사용할 수도 있다. 그러므로, ChannelBuffer의 이름형식은 더이상 의미가 없고, ByteBuf로 변경된 것이다.

새로운 buffer를 생성하는 Utility클래스로서의 ChannelBuffers는 'Unpooled'와 'ByteBufUtil' 2개의 utility 클래스로 분리되었다. 'Unpooled' 는 그 이름에서 유추할 수 있고, ByteBufUtils는 ByteBufAllocator 구현을 통해 만들어진 풀링된 ByteBuf를 생성하는 역할을 한다.

ByteBuf는 인터페이스가 아닌 추상화 클래스이다.
내부 성능테스트에 따르면, ByteBuf를 인터페이스에서 추상화 클래스로 변경한 후 처리량이 약 5%정도 향상되었다.

대부분의 buffer는 최대 용량(capacity)과 함께 유동적이다.
3.x에서는 buffer의 크기는 고정(fixed) 아니면 유동적(dynamic) 이었다. fixed buffer는 한번 생성된 후에는 변경이 되지 않는 반면, dynamic buffer는 write(..) 메소드가 메모리가 더 필요하면 언제든지 크기가 변경되었다.

4.0부터는 모든 buffer는 dynamic buffer 이다. 심지어 예전의 dynamic buffer보다도 더 낫다. 당신은 'ByteBuf.capacity(int newCapacity)' 를 통해 보다 더 쉽게 그리고 크기가 무한히 늘어나지 않도록 최대값까지만 설정을 할 수 있기 때문에 보다 더 안전하게 buffer의 크기를 늘렸다 줄였다 할 수 있다.


 // No more dynamicBuffer() - use buffer().

ByteBuf buf = Unpooled.buffer();

// Increase the capacity of the buffer.
buf.capacity(1024);
...

// Decrease the capacity of the buffer (the last 512 bytes are deleted.) 
buf.capacity(512);


단, wrappedBuffer()로 생성된 single buffer나 byte array는 예외다. 이는 저장된 메모리 복사본처럼 생성된 buffer의 wrapping point를 무효화 시키기 때문에  buffer의 크기를 늘릴 수 없다. buffer를 감싼 후에 크기를 늘리고 싶다면 먼저 충분한 크기의 buffer를 생성한 후, 감싸기 원하는 buffer를 복사를 하면 된다.

새 Buffer 타입: CompositeByteBuf

CompositeByteBuf 라 불리는 새 buffer는 buffer를 합치기 위해 다양한 고급 기능들이 정의되어 있다. 사용자는 상대적으로 비싼 메모리에 무작위 접근하는 부분에서 buffer를 합치는 것을 사용함으로써 복사작업의 벌크메모리를 아낄 수 있다. 합쳐진 buffer를 생성하기 위해서는 앞서 Unpooled.wrappedBuffer(...)처럼  Unpooled.compositeBuffer(...)를 사용하던가 아니면, ByteBufAllocator.compositeBuffer()를 사용하면 된다.


예측가능한 NIO buffer 전환

ChannelBuffer.toByteBuffer()의 계약과 다양함은 3.x에서 충분히 정의가 되지 않았다. 공유된 데이터나 별도의 데이터로 복사된 데이터를 가지고 있는 view buffer를 반환하려고 할 때 사용자가 아는 것은 불가능했다. 4.0에서는

toByteBuffer() 을 ByteBuf.nioBufferCount()nioBuffer(), 그리고 nioBuffers()로 대체했다. 만약 nioBufferCount() 가 0을 반환한다면 사용자는 항상 copy().nioBuffer() 을 호출하여 복사된 buffer를 얻을 수 있다.


리틀 엔디안 지원 변경

리틀 엔디안에 대한 지원도 상당히 변경되었다. 이전에는 사용자가 리틀 엔디안 buffer를 얻기 위해서 LittleEndianHeapChannelBufferFactory 명시하거나 원하는 바이트 오더와 함께 기존 buffer를 감싸도록 되어 있었다. 4.0에서는 ByteBuf.order(ByteOrder) 라는 새로운 메소드가 추가되어서 이를 이용하여 원하는 바이트 오더를 얻을 수 있다.

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.ByteOrder;
 
ByteBuf buf = Unpooled.buffer(4);
buf.setInt(0, 1);
// Prints '00000001'
System.out.format("%08x%n", buf.getInt(0)); 
 
ByteBuf leBuf = buf.order(ByteOrder.LITTLE_ENDIAN);
// Prints '01000000'
System.out.format("%08x%n", leBuf.getInt(0));
 
assert buf != leBuf;
assert buf == buf.order(ByteOrder.BIG_ENDIAN);

풀링된 버퍼들

Netty 4 에서는 buddy allocation 와 slab allocation 을 결합한  jemalloc 의 다양성을 지닌 높은 성능의 buffer pool을 제공한다. 이것은 다음과 같은 이점이 있다.
  • 빈번한 buffer의 할당과 해제로 야기되는 GC부담을 줄여준다.
  • buffer생성시 불가피하게 zero로 채워야 함으로써 야기되는 메모리 공간소비를 줄여준다. 
  • Direct buffer의 해제를 적절한 때에 해 준다.

이러한 이점들을 얻기 위해서는 unpooled buffer를 사용하지 말고  ByteBufAllocator 로부터 buffer를 가져와서 사용하면 된다. 

Channel channel = ...;
ByteBufAllocator alloc = channel.alloc();
ByteBuf buf = alloc.buffer(512);
....
channel.write(buf);
 
ChannelHandlerContext ctx = ...
ByteBuf buf2 = ctx.alloc().buffer(512);
....
channel.write(buf2)

ByteBuf 가 한번 원격에 씌여지면 그 buffer가 원래 있었던 pool에 자동으로 반환될 것이다.


기본 ByteBufAllocator 는 PooledByteBufAllocator 이다. 만약 buffer pooling 이나 당신 스스로가 할당한 것을 사용하기 싫다면 UnpooledByteBufAllocator 처럼 또다른 Allocator인 Channel.config().setAllocator(...) 을 사용하면 된다.


참고 : 기본 allocator UnpooledByteBufAllocator 이더라도 PooledByteBufAllocator 에서 메모리 누수가 없다는 것을 한번 확인한 이상, 다시 PooledByteBufAllocator 로 기본값을 설정할 것이다.

ByteBuf 는 항상 참조 카운트가 계산된다.

더 많은 예측가능한 상황에서의 ByteBuf 의 라이프 사이클을 다루기 위해 Netty는 확실한 참조를 가지고 있는 경우를 제외하고는 더이상 가비지 콜렉터에 의지하지 않는다. 여기에 기본 규칙이 있다. :
  • buffer가 할당되었을 때 초기 참조 카운트는 1이다.
  • buffer의 참조 카운트가 0으로 줄어들면 해제되거나 사용되었던 pool로 반환된다.
  • 다음의 시도들은 IllegalReferenceCountException를 발생시킨다.
    • 참조 카운트가 0인 buffer에 접근하려 할 때,
    • 참조 카운트가 음수로 감소하거나,
    • Integer.MAX_VALUE 를 넘어설 때.
  • slices되거나 복제된 것과 같이 파생된 buffer와 리틀 엔디안으로 스왑된 buffer는 원래 buffer와 함께 참조 카운터를 공유한다. 참고로 파생 buffer가 생성될 때는 참조 카운트가 변하지 않는다.

 ByteBuf 가 ChannelPipeline 에서 사용될 때는, 아래처럼 추가적인 규칙이 있다는 것을 명심할 필요가 있다.

  • 파이프라인에 있는 각각의 인바운드 핸들러(업스트림 같은)는 수신된 메시지를 해제 하는데, Netty는 당신을 위해 자동으로 해제 하지 않는다.

    • 코덱 프레임워크에서 메시지를 자동으로 해제했을 경우, 여러분이 다음 핸들러로 해당 메시지를 그대로 전달하기를 원한다면 참조 카운트를 증가시켜주어야 한다.
  • 아웃바운드(다운스트림 같은) 메시지가 파이프라인의 처음에 도달했을 때, Netty는 메시지가 씌여진 후 그 메시지를 해제 할 것이다. 

자동 버퍼 누수 감지

비록 레퍼런스 카운팅이 좋다고 하더라도, 이 또한 문제는 있다. 개발자가 buffer를 어디서 해제해야 하는지를 찾는 것을 도와주기 위해 누수감지자가 자동으로 누수된 버퍼가 할당된 위치를 추적하는 것을 기록한다.

누수감지자는 PhantomReference 에 의존하고 추적하는 것이 상당히 비싼 작업이기 때문에 단지 할당된 것들 중에 대략 1%정도만 추출된다. 고로 가능한 모든 누수를 찾기 위해서는 상당히 오랜 시간동안 어플리케이션을 실행시키는 것이 더 좋은 방법이다.

일단 모든 누수가 발견되고 수정이 되는데, 개발자는 런타임시에 발생되는 오버헤드를 방지하기 위해 JVM 옵션(-Dio.netty.noResourceLeakDetection )을 주어서 이 기능을 끌 수도 있다.

io.netty.util.concurrent

새 독립적인 buffer API에 따라 4.0에서는 io.netty.util.concurrent 패키지를 통해 비동기 어플리케이션에서 유용하게 사용될 다양한 클래스를 제공한다. 일부 클래스들은 아래와 같다.

  • Future and Promise - ChannelFuture 와 유사하나 Channel 에 의존적이지 않다.
  • EventExecutor and EventExecutorGroup - 일반적인 이벤트 loop API

이 클래스들은 이 문서 끝부분에서 설명될 channel API 에서 기본으로 사용된다. 예를 들면, ChannelFuture 는 io.netty.util.concurrent.Future 를 상속받고, EventLoopGroup 는 EventExecutorGroup 를 상속받는 것 같은 것이다.     



Channel API 변화

4.0에서는 io.netty.channel 패키지 아래에 있는 많은 클래스들이 대 정비를 통해 없어졌다. 그래서 단순히 코드만를 검색하고 변경하는 것만으로는 기존 3.x 어플리케이션을 4.0으로 동작하게 하게 할 수 없다. 이번 장에서는 변경된 것들에 대한 하나하나의 내용들 보다는 그러한 큰 변화들 뒤에 있는 고민에 흔적들에 대해서 알아 볼 것이다.  


ChannelHandler 인터페이스의 변경

Upstream → Inbound, Downstream → Outbound

'upstream'과 'downstream'은 초보자에게 상당히 헷갈리는 단어다. 4.0에서는 가능하면 언제나 'inbound', 'outbound'를 사용한다.


새로운 ChannelHandler 타입 계층

3.x에서는 ChannelHandler 는 그저 tag 인터페이스(메소드가 없는) 였고, ChannelUpstreamHandler 와 ChannelDownstreamHandler , LifeCycleAwareChannelHandler 에서 실제 메소드들을 정의했다. Netty 4에서는 ChannelHandler 는 inbound와 outbound에서 유용하게 사용될 많은 메소드들과 함께 LifeCycleAwareChannelHandler 과 합쳐졌다.

public interface ChannelHandler {
    void handlerAdded(ChannelHandlerContext ctx) throws Exception;
    void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

다음의 다이어그램이 새로운 타입의 계층을 보여준다.



Event object 가 없는 ChannelHandler

3.x에서는 모든 I/O 처리시에 ChannelEvent 객체를 생성했다. 읽고 쓰기를 위해서 추가적으로 ChannelBuffer 도 생성했다. 이는 JVM에게 리소스 관리와 버퍼풀링을 위임함으로써 Netty의 내부를 아주 많이 단순화 했다. 그러나 이는 종종 Netty기반의 어플리케이션이 과부하에 있을 때 때때로 보여지는 부담(pressure)되고 불안정한 GC를 야기시키는 원인이었다.


4.0에서는 엄격한 타입의 메소드 실행과 함께 event object를 대신 함으로써 거의 완벽하게 생성된 event object를 제거한다. 3.x에서는 handleUpstream() 와 handleDownstream() 처럼 모든 이벤트를 잡을 수 있는 이벤트 핸들러 메소드를 가지고 있었지만 이제는 더이상 그렇지 않다. 모든 이벤트 타입은 각자의 핸들러 메소드를 갖게 된다.

// Before:
void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e);
void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e);
 
// After:
void channelRegistered(ChannelHandlerContext ctx);
void channelUnregistered(ChannelHandlerContext ctx);
void channelActive(ChannelHandlerContext ctx);
void channelInactive(ChannelHandlerContext ctx);
void channelRead(ChannelHandlerContext ctx, Ojbect message);
 
void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise);
void connect(
        ChannelHandlerContext ctx, SocketAddress remoteAddress,
        SocketAddress localAddress, ChannelPromise promise);
void disconnect(ChannelHandlerContext ctx, ChannelPromise promise);
void close(ChannelHandlerContext ctx, ChannelPromise promise);
void deregister(ChannelHandlerContext ctx, ChannelPromise promise);
void write(ChannelHandlerContext ctx, Object message, ChannelPromise promise);
void flush(ChannelHandlerContext ctx);
void read(ChannelHandlerContext ctx);

ChannelHandlerContext 또한 위에서 언급한 변화들을 반영하기 위해 변경되었다.

// Before:
ctx.sendUpstream(evt);
 
// After:
ctx.fireChannelRead(receivedMessage);

이러한 모든 변화들은 개발자가 더이상 존재하지 않는 ChannelEvent 인터페이스를 상속받을 수 없다는 것을 의미한다. 그러면 어떻게 개발자가 IdleStateEvent 와 같은 이벤트 타입을 정의할 수 있을까? 4.0에서 ChannelInboundHandler 는 이러한 특정한 개발자의 경우를 위해 userEventTriggered() 라는 메소드를 제공한다.


간단한 채널 상태 모델

3.x에서 새로 연결된 Channel이 생성될 때, 적어도 3번의 ChannelStateEvent 가 발생한다.(channelOpenchannelBoundchannelConnected) 

Channel 이 닫힐 때도 역시나 적어도 3번 이상의 이벤트가 발생한다.

(channelDisconnectedchannelUnbound, and channelClosed)



하지만 이렇게 많은 이벤트가 발생하는 건 좀 모호하다. 그래서 개발자에게 채널이 읽거나 쓰는 것을 수행할 수 있는 상태에 진입했을 때를 알려주는 것이 더 유용하다.



channelOpenchannelBoundchannelConnected 은 channelActive 로 합쳐졌고,

channelDisconnectedchannelUnboundchannelClosed 은 channelInactive 로 합쳐졌다. 역시나 마찬가지로 Channel.isBound(),isConnected() 은 isActive() 로 합쳐졌다.

참고로, channelRegistered 과 channelUnregistered 는 channelRegistered,channelUnregistered 과 같진 않다. 이들은 아래 그림과 같이 Channel 의 동적인 등록, 등록취소, 재등록을 지원하는 새로운 상태들이다.


* wiz talk ==================

사실 위의 그림이 이해가 잘 안된다. channelRegistered_2 이후에도 channelActive 여야 하는 게 아닌가..?

=========================


write() 는 자동으로 flush() 되지 않는다.

4.0에서는 Channel 의 outbound buffer를 확실하게 내보내는(flushes) flush() 라는 새로운 메소드를 제공하고, write() 은 더이상 자동으로 flush 하지 않는다. 메세지 레벨에서 동작하는 것을 제외한다면, 언뜻  java.io.BufferedOutputStream 을 생각할 수 있겠다.


이러한 변화로 인해 개발자는 무언가를 writing한 후에는  ctx.flush() 를 호출하는 것을 잊지 않도록 주의해야 할 것이다. 대안으로 writeAndFlush() 을 사용할 수도 있다.


합리적이고 에러가 적게 발생하는 inbound traffic 중지

3.x는 Channel.setReadable(boolean) 을 통해 제공되는 직관적인 inbound traffic suspension 메카니즘을 제공했다. 이것은 채널핸들러와 각각 잘못 구현되어 서로 방해가 되기 쉬운 핸들러들 사이에 복잡한 상호작용을 야기시켰다.


4.0에서는 read() 라고 불리는 새로운 outbound 기능이 추가되었다. 만약 개발자가 기본값인 auto-read 플래그를  Channel.config().setAutoRead(false) 를 이용해서 비활성화 시킨다면, Netty는 개발자가 read() 를 호출하지 않는 이상 어떠한 것도 읽지 않을 것이다. 개발자가 한번 실행시킨 read() 가 끝나면 해당 채널은 다시 읽는 것을 멈추고 channelReadSuspended() 라는 inbound 이벤트가 호출되고 개발자는 다시 read() 를 호출할 수 있다. 개발자는 또한 더 훌륭한 traffic 제어를 수행하기 위해 read() 기능을 가로챌 수도 있다.


incoming 연결 수락의 중지

Netty 3.x 에서는 서버소켓이 끊기거나 I/O 스레딩이 블록이 되는 경우를 제외하고는 개발자가 incoming 연결을 멈추기 위한 방법이 없었다. 4.0에서는 auto-read 플래그가 셋팅되지 않았을 때 그냥 일반 채널처럼 read() 를 중시한다.


Half-closed 소켓

TCP와 SCTP 는 개발자가 소켓이 완전히 닫히지 않았어도 outbound 트래픽을 셧다운 할 수 있도록 한다. 이러한 소켓을 'half-closed' 소켓이라 하고 개발자는 SocketChannel.shutdownOutput() method 를 호출해서 'half-closed' 소켓을 만들 수 있다. 원격지에서 겉으로는 연결이 끊겼는지 구별이 힘든 outbound 트래픽을 셧다운 하면 SocketChannel.read(..) 은 -1을 리턴한다.


다루기 쉬운 I/O 스레드 할당

3.x에서는 Channel 은 ChannelFactory 로 생성이 되고 새로 생성된 Channel 은 자동으로 숨겨진 I/O 스레드에 등록된다. 4.0에서는 ChannelFactory 을 한개 이상의 EventLoops 로 구성된  EventLoopGroup 인터페이스로 대체했다. 또한 Channel 은 자동으로 EventLoopGroup 에 등록되지 않고 개발자가 EventLoopGroup.register() 을 호출해야 한다.

ChannelFactory 와 I/O 스레드의 분리는 잘 된 일이다. 개발자는 다른 Channel 구현체를 같은 EventLoopGroup 에 등록할 수 있고 같은 Channel 구현체를 다른 EventLoopGroups 에 등록할 수 있다. 예를 들면, 개발자는 같은 I/O 스레드에 NIO 서버소켓, NIO클라이언트 소켓, NIO UDP 소켓, VM local 채널을 실행시킬 수 있다. 이것은 최소한의 지연을 요구하는 프록시 서버를 만들 때 매우 유용할 것이다.


JDK에서 제공되는 Socket으로 Channel 생성

3.x에서는 java.nio.channels.SocketChannel 와 같이 JDK에 있는 것을 이용해서 channel을 생성하는 방법이 없었으나, 4.0에서는 가능하다.


I/O 스레드로부터 Channel의 등록해제와 재등록

3.x에서는 Channel 을 한번 생성하면 그 channel의 소켓이 닫힐 때까지 싱글 I/O 스레드에 묶여 있게 된다. 4.0에서는 개발자가 I/O 스레드로부터 해당 channel의 JDK 기반 소켓의 모든 제어를 하기 위해 channel을 등록해제 할 수 있다. 예를 들면, 개발자는 복잡한 프로토콜을 다루기 위해 Netty에서 제공하는 non-blocking I/O 기능을 활용한 후, 해당 Channel 을 등록해제 후에 가능한 최대 처리량에서 파일 이동을 하기 위해  blocking 모드로 전환할 수가 있다.

java.nio.channels.FileChannel myFile = ...;
java.nio.channels.SocketChannel mySocket = java.nio.channels.SocketChannel.open();
 
// Perform some blocking operation here.
...
 
// Netty takes over.
SocketChannel ch = new NioSocketChannel(mySocket);
EventLoopGroup group = ...;
group.register(ch);
...
 
// Deregister from Netty.
ch.deregister().sync();
 
// Perform some blocking operation here.
mySocket.configureBlocking(false);
myFile.transferFrom(mySocket, ...);
 
// Register back again to another event loop group.
EventLoopGroup anotherGroup = ...;
anotherGroup.register(ch);

* wiz talk ==========

소켓 하나로 blocking, non-blocking 작업을 번갈아 가며 할 수 있다는 얘기(?) 인 듯. 가슴에 와닿지는 않음. ㅜㅜ;

=================


I/O 스레드에 의해 실행되는 임의의 작업 스케줄링

Channel 이 EventLoopGroup 에 등록될 때, Channel 은 사실 EventLoopGroup 에 의해 관리되는 EventLoops 의 하나로 등록된다. EventLoop 은  java.util.concurrent.ScheduledExecutorService 을 구현했는데, 이는 개발자가 그가 만든 channel이 속한 I/O 스레드 내에서 Runnable 이나 Callable 을 실행하거나 스케줄링 할 수 있다는 것을 의미한다. 아주 잘 정의된 스레드 모델-이는 나중에 설명하겠지만-에 의해서 thread-safe handler를 만드는 것이 아주 쉽게 되었다.

public class MyHandler extends ChannelOutboundHandlerAdapter {
    ...
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise p) {
        ...
        ctx.write(msg, p);
        
        // Schedule a write timeout.
        ctx.executor().schedule(new MyWriteTimeoutTask(p), 30, TimeUnit.SECONDS);
        ...
    }
}
 
public class Main {
    public static void main(String[] args) throws Exception {
        // Run an arbitrary task from an I/O thread.
        Channel ch = ...;
        ch.executor().execute(new Runnable() { ... });
    }
}

* wiz talk ==========

이 부분 역시 가슴에 와닿지는 않음. ㅜㅜ; 무슨 의미인지..

=================

 

단순해 진 셧다운

더이상 releaseExternalResources() 을 사용할 필요없이 개발자는 모든 열려진 channel을 즉시 닫을 수 있고 EventLoopGroup.shutdownGracefully() 을 호출함으로써 모든 I/O 스레드들이 스스로 정지하도록 할 수 있다.


안전한 타입의 ChannelOption

Netty에서는 Channel 의 소켓 파라미터를 설정하는데는 두가지 방식이 있다. 하나는 SocketChannelConfig.setTcpNoDelay(true) 처럼 ChannelConfig 을 호출하는 것이고 이건 아주 안전한 타입의 방법이다.  다른 하나는 ChannelConfig.setOption() 을 호출하는 것이다. 가끔 개발자는 런타임시에 소켓 옵션을 정해야 하는 경우가 있는 데, 이 메소드는 이런 경우에 이상적이다. 그러나 3.x에서는 개발자가 문자열과 객체로 쌍을 이룬 것으로 옵션을 설정해야 해서 에러가 발생되었는데, 개발자가 잘못된 옵션이름이나 값을 호출하면 ClassCastException 가 발생하게 되거나 심지어는 설정한 옵션들이 조용히 무시가 되는 현상이 발생되었다.

이에 4.0에서는 소켓옵션에서의 안전한 타입을 제공하는  ChannelOption 이라는 새로운 타입을 제시한다.

ChannelConfig cfg = ...;
 
// Before:
cfg.setOption("tcpNoDelay", true);
cfg.setOption("tcpNoDelay", 0);  // Runtime ClassCastException
cfg.setOption("tcpNoDelays", true); // Typo in the option name - ignored silently
 
// After:
cfg.setOption(ChannelOption.TCP_NODELAY, true);
cfg.setOption(ChannelOption.TCP_NODELAY, 0); // Compile error


속성맵

개발자의 요구에 대한 응답으로 개발자는 Channel 와 ChannelHandlerContext 에 어떠한 객체라도 추가할 수 있는데, Channel 와 ChannelHandlerContext 을 상속받은 AttributeMap 라는 새로운 인터페이스가 추가되었다. 대신에 ChannelLocal 과 Channel.attachment 은 삭제되었다. 속성값들은 그 값들이 속한 Channel 이 GC가 될 때 같이 GC가 된다.

public class MyHandler extends ChannelInboundHandlerAdapter {
 
    private static final AttributeKey<MyState> STATE =
            new AttributeKey<MyState>("MyHandler.state");
 
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) {
        ctx.attr(STATE).set(new MyState());
        ctx.fireChannelRegistered();
    }
 
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        MyState state = ctx.attr(STATE).get();
    }
    ...
}


새로운 부트스트랩 API

부트스트랩 API는 기존 것과 역할은 같을 지라도 다시 작성되었다. 이 API는 종종 상투적인 코드로 보여지는, 서버나 클라이언트를 준비시키고 실행시키기 위해 요구되는 대표적인 단계들을 수행한다.


새로운 부트스트랩 API 또한 유창하게 인터페이스를 제공한다.

public static void main(String[] args) throws Exception {
    // Configure the server.
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .option(ChannelOption.SO_BACKLOG, 100)
         .localAddress(8080)
         .childOption(ChannelOption.TCP_NODELAY, true)
         .childHandler(new ChannelInitializer<SocketChannel>() {
             @Override
             public void initChannel(SocketChannel ch) throws Exception {
                 ch.pipeline().addLast(handler1, handler2, ...);
             }
         });
 
        // Start the server.
        ChannelFuture f = b.bind().sync();
 
        // Wait until the server socket is closed.
        f.channel().closeFuture().sync();
    } finally {
        // Shut down all event loops to terminate all threads.
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
        
        // Wait until all threads are terminated.
        bossGroup.terminationFuture().sync();
        workerGroup.terminationFuture().sync();
    }
}

ChannelPipeLineFactory -> ChannelInitializer

위의 코드에서 눈치 챘듯이 ChannelPipelineFactory 은 이제 없고, Channel 과 ChannelPipeline 을 더 잘 제어할 수 있는 ChannelInitializer 로 대체되었다. 


개발자 스스로가 ChannelPipeline 을 생성하지 않음에 주목하자. 지금까지 오랫동안 관찰된 많은 경우들을 보고 받은 후에 Netty프로젝트 팀은 개발자 스스로가 pipeline을 구현하거나 기본 구현된 것을 상속받는 것은 이점이 없다는 결론을 내렸다. 그래서 ChannelPipeline 는 더이상 개발자에 의해 생성되지 않는다. ChannelPipeline은 Channel 에 의해 자동으로 생성된다. 


ChannelFuture -> ChannelFuture 와 ChannelPromise

ChannelFuture 은 ChannelFuture 와 ChannelPromise 로 나뉘어 졌다. 이는 비동기 기능을 명시하는  생산자와 소비자의 축소 뿐만 아니라 ChannelFuture 은 그 상태값이 변경될 수 없기 때문에 필터링처럼 체인에서 반환된 ChannelFuture 을 사용하기 위해 더 안전하다. 

이러한 변화로 인해, ChannelFuture 의 상태를 변경하기 위해 몇몇 메소드들은 ChannelFuture 보다는 ChannelPromise 를 수용한다.


잘 정의된 스레드 모델

비록 3.5에서 모순된 스레드 모델을 수정하려 했던 시도는 있었지만 3.x에서는 잘 정의된 스레드 모델은 없었다. 4.0에서는 개발자가 스레드 안전에 대해 너무 많은 고민을 하지 않고 ChannelHandler 을 쓰는 것을 도와주도록 엄격한 스레드 모델을 정의했다.

  • Netty에서는 ChannelHandler 에 @Shareable 이 있지 않으면 절대로 ChannelHandler 의 메소드를 동시에 호출하지 않을 것이다. 이것은 inbound, outbound, life cycle 이벤트 핸들러 같은 메소드의 형식에 개의치 않는다.
    • 개발자는 이제 더이상 inbound나 outbound 이벤트 핸들러 메소드를 동기화 할 필요가 없다.
    • 4.0에서는 @Sharable 의 명시없이는 ChannelHandler 는 한번만 추가하도록 한다.
  • Netyy에 의해 만들어진 각 ChannelHandler 사이에서의 메소드 호출은 항상  happens-before 한 관계이다.
    • 개발자는 핸들러의 상태를 유지하기 위해 volatile 필드를 정의할 필요가 없다.
  • 개발자는 ChannelPipeline 에 핸들러를 추가할 때, EventExecutor 를 명시할 수 있다.
    • 명시를 했다면, ChannelHandler 의 메소드들은 명시한 EventExecutor 에 의해 실행 된다.
    • 만약, 명시하지 않았다면, 핸들러 메소드들은 항상 그 핸들러와 관계된 Channel 이 등록된 EventLoop 에 의해 실행 된다. 
  • 핸들러나 channel에 할당된 EventExecutor 와 EventLoop 는 항상 싱글 스레드 이다.
    • 핸들러 메소드들은 항상 같은 스레드에 의해 실행된다.
    • EventExecutor 나 EventLoop 가 멀티스레드로 되어 있다면, 먼저 스레드 들 중에 하나가 선택되고 그 스레드가 등록취소(deregistration) 될 때까지 사용될 것이다.
    • 같은 pipeline에 두 개의 핸들러가 다른 EventExecutors 과 함께 할당되어 있다면, 그 핸들러들은 동시에 실행된다. 한 개 이상의 핸들러가 공유 데이터에 접근을 한다면(그 공유 데이터가 같은 pipeline에 등록된 핸들러들에 의해서만 접근이 된다 하더라도!) 개발자는 thread-safe에 신경을 써야 한다.
  •  ChannelFuture 에 추가된 ChannelFutureListeners 는 항상 Channel 과 관계된 future가 할당된  EventLoop 스레드에 의해 실행된다.

더 이상 ExecutionHandler는 없다. 그것은 core의 일부가 됨
개발자는 ChannelPipeline 에 ChannelHandler 을 추가할 때, EventExecutor 을 명시 할 수 있고, 그 명시된 EventExecutor 을 통해서 추가된 ChannelHandler 의 메소들을 항상 실행할 수 있다.
Channel ch = ...;
ChannelPipeline p = ch.pipeline();
EventExecutor e1 = new DefaultEventExecutor(16);
EventExecutor e2 = new DefaultEventExecutor(8);
 
p.addLast(new MyProtocolCodec());
p.addLast(e1, new MyDatabaseAccessingHandler());
p.addLast(e2, new MyHardDiskAccessingHandler());

코덱 프레임워크의 변화
4.0에서는 핸들러의 버퍼(이 문서의 buffer 섹션을 보라.)를 생성하고 관리해야 하는 핸들러를 요구했기 때문에, 코덱 프레임워크 내에서 많은 변화들이 있었다. 하지만, 개발자들의 입장에서의 변화는 그다지 크지는 않다.
  • 핵심 코덱 클래스들은 io.netty.handler.codec 패키지로 이동 되었다.
  • FrameDecoder 는 ByteToMessageDecoder 로 이름이 변경 되었다.
  • OneToOneEncoder 와 OneToOneDecoder 는 MessageToMessageEncoder 와 MessageToMessageDecoder 로 대체 되었다.
  • decode()decodeLast()encode() 의 메소드 표기는 일반적인 지원과 많은 파라미터를 삭제 등 조금 변경되었다.

Codec embedder → EmbeddedChannel

코덱 embedder는 코덱을 포함한 어떤 pipeline이라도 이를 개발자가 테스트를 할 수 있게 하도록  io.netty.channel.embedded.EmbeddedChannel 로 대체 되었다.

HTTP Codec

HTTP 디코더는 항상 하나의 HTTP 메세지에 대해 여러 메세지 객체를 생성한다.

1       * HttpRequest / HttpResponse
0 - n   * HttpContent
1       * LastHttpContent

좀더 자세히 보려면, 수정된 HttpSnoopServer 예제를 참고하면 된다. 만약 하나의 HTTP 메세지에 대해 여러 메세지 객체를 생성하는 것을 원하지 않는다면, 개발자는 pipeline에 HttpObjectAggregator 를 추가하면 된다. HttpObjectAggregator 는 여러 메세지를 FullHttpRequest 나 FullHttpResponse 로 변환시켜 준다.

전송계층(transport) 구현에 대한 변화

다음 전송계층(transport)이 새롭게 추가되었다.

  • OIO SCTP transport
  • UDT transport

사례 연구 : Factorial 예제를 포팅하기

이 섹션에서는 3.x에 있는 Factorial 예제를 4.0으로 포팅하기 위한 대략적인 단계를 보여줄 것이다. Factorial 예제는 이미 4.0의 io.netty.example.factorial 패키지에 포팅 되어 있다. 변경된 모든 부분을 보기 위해서는 소스 코드를 보길 바란다.

    서버 Porting

    1. 새로운 부트스트랩 API를 사용하기 위해 FactorialServer.run() 메소드를 다시 쓴다.
      1. 더이상 ChannelFactory 는 없다. 대신 NioEventLoopGroup (커넥션을 받아들이는 것 하나와 커넥션들을 다루기 위한 또다른 하나) 을 사용한다.
    2. FactorialServerPipelineFactory 을 FactorialServerInitializer 로 이름을 변경한다.
      1. ChannelInitializer<Channel> 을 상속 받는다.
      2. ChannelPipeline 를 생성하는 대신에, Channel.pipeline() 을 이용하여 pipeline을 얻는다.
    3. ChannelInboundHandlerAdapter 을 상속받은 FactorialServerHandler 을 만든다.
      1. channelDisconnected() 은 channelInactive() 로 대체한다.
      2. handleUpstream() 는 더 이상 사용하지 않는다.
      3. messageReceived() 은 channelRead() 으로 이름을 변경하고, 그에 맞게 메소드 표기방식을 수정한다.
      4. ctx.write() 는 ctx.writeAndFlush() 로 이름을 변경한다.
    4. ByteToMessageDecoder<BigInteger> 를 상속받은 BigIntegerDecoder 를 만든다.
    5. MessageToByteEncoder<Number> 를 상속받은 NumberEncoder 를 만든다.
      1. encode() 는 더이상 버퍼를 반환하지 않고, ByteToMessageDecoder 가 제공하는 버퍼에 인코딩된 데이터를 채운다.

    클라이언트 Porting

    위의 서버 포팅과 유사하지만, 만약에 큰 스트림 데이터를 쓸 때는 주의를 해야 한다.

    1. 새로운 부트스트랩 API를 위해 FactorialClient.run() 메소드를 다시 쓴다.
    2. FactorialClientPipelineFactory 를 FactorialClientInitializer 로 이름을 변경한다.
    3. ChannelInboundHandlerAdapter 를 상속받은 FactorialClientHandler 를 만든다.



    댓글
    댓글쓰기 폼