IT

Netty ChannelHandler와 ChannelPipeline

코딩하는 너구리 2021. 1. 3. 23:53
반응형

 

 

이미 앞부분에서 ChannelPipeline안에 ChannelHandler를 체인으로 연결해 처리 논리를 구성할 수 있다는 것을 배웠다. 이러한 클래스와 연관된 사용사례들과 ChannelHandlerContext에 대해서도 알아보자.

 

 

Channel의 수명주기

Channel 인터페이스는 ChannelInboundHandler API와 밀접한 관계가 있으며 간단하지만 유용한 상태 모델을 정의한다.

Channel의 네 가지 상태에 대해 알아보자.

 

상태 설명
ChannelUnregistered Channel이 생성됐지만 EventLoop에 등록되지 않음
ChannelRegistered Channel이 EventLoop에 등록됨
ChannelActive Channel이 활성화됨(원격 피어로 연결됨). 이제 데이터를 주고받을 수 있음
ChannelInactive Channel이 원격 피어로 연결되지 않음

 

Channel의 일반적인 생명 주기는

ChannelRegistered ==> ChannelActive ==> ChannelInactive ==> ChannelUnregistered 이다.

 

 

 

ChannelHandler 수명주기

Channelhandler 인터페이스에서 정의하는 수명주기 메서드에 대해 알아보자. 이러한 메서드는 ChannelHandler가 ChannelPipeline에 추가 또는 제거된 후 호출된다. 각 메서드는 ChannelHandlerContext 인수를 받는다.

 

메서드 설명
handlerAdded Channelhandler가 ChannelPipeline에 추가될 때 호출됨.
handlerRemoved Channelhandler가 ChannelPipeline에서 제거될 때 호출됨.
exceptionCaught ChannelPipeline에서 처리 중에 오류가 발생하면 호출됨.

 

네티는 다음과 같이 ChannelHandler의 가장 중요한 하위 인터페이스를 정의한다.

 

  • ChannelInboundHandler
    • 모든 유형의 인바운드 데이터와 상태 변경을 처리한다.
  • ChannelOutboundHandler
    • 아웃바운드 데이터를 처리하고 모든 작업의 가로채기를 허용한다.

 

 

다음으로 이러한 ChannelInboundHandlerChannelOutboundHandler에 대해 알아보자.

ChannelInboundHandler 인터페이스

메서드 설명
channelRegistered Channel이 EventLoop에 등록되고 입출력을 처리할 수 있으면 호출됨
channelUnregistered Channel이 EventLoop에서 등록 해제되고 입출력을 처리할 수 없으면 호출됨
channelActive Channel의 연결과 바인딩이 완료되어 활성화되면 호출됨
channelInactive Channel이 활성 상태에서 벗어나 로컷 피어에 대한 연결이 해제되면 호출됨
channelReadComplete Channel에서 읽기 작업이 완료되면 호출됨
channelRead Channel에서 데이터를 읽을 때 호출됨
channelWritabilityChanged Channel의 기록 가능 상태가 변경되면 호출된다.
userEventTriggered POJO가 ChannelPipeline을 통해서 전달돼서 ChannelInboundHandler.fireUserEventTriggered()가 트리거되면 호출됨.

ChannelInboundHandler 구현이 channelRead()를 재정의하는 경우 풀링된 ByteBuf 인스턴스의 메모리를 명시적으로 해제하는 역할을 맡는다.

 

 

네티는 다음 예제와 같이 메모리를 해제할 수 있는 ReferenceCountUtil.release() 메서드를 제공한다.

@Sharable
public class DiscardHandler extends ChannelInboundHandlerAdapter {    // ChannelInboundHandlerAdapter를 확장
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {    // 수신한 메시지를 삭제
        ReferenceCountUtil.release(msg);
    }
}

 

 

네티는 해제되지 않은 리소스WARN 수준 로그 메시지로 로깅하므로 코드에 문제가 되는 인스턴스가 있으면 쉽게 발견할 수 있다. 그러나 이렇게 매번 리소스를 관리하기는 번거로울 수 있으며, SimpleChannelInboundHandler를 이용하면 더 쉽게 리소스를 관리할 수 있다. 다음 예제를 보자.

 

 

@Sharable
public class SimpleDiscardhandler extends SimpleChannelInboundHandler<Object> {    // SimpleChannelInboundHandler를 확장
    @Override
    public void channelRead0(ChannelHandlerContext ctx, Object msg) {
        // 다른 조치를 취할 필요가 없음
    }
}

 

SimpleChannelInboundHandler리소스를 자동으로 해제하므로 메시지의 참조도 무효화된다. 즉, 메시지의 참조를 저장해 나중에 이용하려고 하면 안 된다.

 

 

 

ChannelOutboundHandler 인터페이스

아웃바운드 작업과 데이터는 ChannelOutboundHandler에 의해 처리되며, 여기에 포함된 메서드는 Channel, ChannelPipeline, ChannelHandlerContext에서 이용된다.

 

ChannelOutboundHandler는 주문식으로 작업이나 이벤트를 지연하는 강력한 기능이 있어 정교하게 요청을 처리할 수 있다. 예를 들어 원격 피어에 대한 기록이 일시 중단된 경우 플러시 작업을 지연하고 나중에 재개할 수 있다.

 

ChannelOutboundHandler 메서드

메서드 설명
bind(ChannelHandlerContext, SocketAddress, ChannelPromise) Channel을 로컬 주소로 바인딩 요청 시 호출됨
connect(ChannelHandlerContext, SocketAddress, SocketAddress, ChannelPromise) Channel을 원격 피어로 연결 요청 시 호출됨
disconnect(ChannelHandlerContext, ChannelPromise) Channel을 원격 피어로부터 연결 해제 요청 시 호출됨
close(ChannelhandlerContext, ChannelPromise) Channel을 닫는 요청 시 호출됨
deregister(ChannelHandlerContext, ChannelPromise) Channel을 EventLoop에서 등록 해제 요청 시 호출됨
read(ChannelHandlerContext) Channel에서 데이터를 읽기 요청 시 호출
flush(ChannelHandlerContext) Channel을 통해 원격 피어로 큐에 있는 데이터의 플러시 요청 시 호출됨
write(ChannelHandlerContext, Object, ChannelPromise) Channel을 통해 원격 피어로 데이터 기록 요청 시 호출됨

 

 

 

 

ChannelPromise와 ChannelFuture 비교

ChannelOutboundHandler에 있는 대부분의 메서드에는 작업이 완료되면 알림을 전달받을 ChannelPromise 인수가 있다. ChannelPromise는 ChannelFuture의 하위 인터페이스로서 setSuccess()나 setFailure() 같은 기록 가능 메서드를 정의해 ChannelFuture를 읽기 전용으로 만든다.

 

 

 

 

리소스 관리

ChannelInboundHandler.channelRead() 또는 ChannelOutboundHandler.write()를 호출해 데이터를 대상으로 작업할 때는 리소스 누출이 발생하지 않게 주의해야 한다.

네티는 참조 카운팅을 이용해 풀링되는 ByteBuf를 관리한다. 네티는 잠재적인 문제 진단을 돕기 위해 애플리케이션 버퍼 할당의 약 1%를 샘플링해 메모리 누출을 검사하는 ResourceLeakDetector 클래스를 제공한다.

 

 

 

 

인바운드 메시지를 소비하는 쉬운 방법

인바운드 데이터를 소비하고 해제하는 것은 아주 흔한 작업이다. 네티는 이 작업을 위해 SimpleChannelInboundHandler라는 특수한 ChannelInboundHandler 구현을 제공한다. 이 구현은 메시지가 ChannelRead0()에서 소비되면 자동으로 메시지를 해제한다.

인바운드 핸들러를 활용할 때 기본적으로 SimpleChannelInboundHandler를 사용하는 습관을 들여 손쉬운 리소스 관리를 하도록 하자.

 

 

아웃바운드에서의 리소스 관리

아웃바운드 핸들러에서는 write() 작업을 처리하고 메시지를 폐기하는 경우 직접 메시지를 해제해야 한다.

또한 리소스를 해제하는 것 뿐만 아니라 ChannelPromise에 알리는 것도 중요하다.

ChannelFutureListener가 메시지가 처리된 것에 대한 알림을 받지 못하는 경우가 생길 수 있기 때문이다.

따라서, 정리하자면 메시지가 소비 또는 폐기되어 다음 ChannelOutboundHandler로 전달되지 않는 경우 직접 ReferenceCountUtil.release()를 호출해야 한다. 전송 레이어에 도달한 메시지는 기록될 때 또는 Channel이 닫힐 때 자동으로 해제된다.

 

 

 

 

ChannelPipeline

ChannelPipelineChannel을 통해 오가는 인바운드와 아웃바운드 이벤트를 가로채는 Channelhandler 인스턴스의 체인이라고 생각하면, Channelhandler의 상호작용을 쉽게 이해할 수 있다.

 

새로운 Channel이 생성될 때마다 새로운 Pipeline이 생성된다. 이 연결은 영구적이며 Channel을 다른 ChannelPipeline과 연결하거나 현재 연결을 해제할 수 없다.

 

다음 사항들을 기억해두자.

  • ChannelPipeline은 한 Channel과 연결된 여러 ChannelHandler를 포함한다.
  • 필요에 따라 동적으로 ChannelHandler를 추가하고 제거해 동적으로 ChannelPipeline을 수정할 수 있다.
  • ChannelPipeline에는 인바운드와 아웃바운드 이벤트에 반응해 작업을 호출하는 풍부한 API가 있다.

 

 

ChannelHandlerContext

ChannelHandler는 ChannelHandlerContext를 이용해 해당 ChannelPipeline 및 다른 핸들러와 상호작용할 수 있다. ChannelPipeline의 다음 ChannelHandler에 알림을 전달하는 것은 물론, 속해 있는 ChannelPipeline을 동적으로 수정할수도 있다.

 

Channel이나 ChannelPipeline 인스턴스에서 호출하면 메서드가 전체 파이프라인을 통해 전파된다.

 

반면 동일한 메서드를 ChannelHandlerContext에서 호출하면 현재 연결된 ChannelHandler에서 시작하며 파이프라인에서 이벤트를 처리할 수 있는 다음 ChannelHandler로만 전파된다.

 

따라서 이벤트 흐름이 짧은 ChannelHandlerContext를 잘 활용하면 성능상 이익을 거둘 수 있다.

 

 

 

Channel, ChannelPipeline, ChannelHandlerContext에 있는 메서드 동작에 대해 알아보자.

다음 예제는 ChannelHandlerContext에서 Channel에 대한 참조를 얻은 다음, 해당 Channel에서 write()를 호출해 이벤트가 파이프라인 전체를 통과하게 한다.

ChannelHandlerContext ctx = ...;
Channel channel = ctx.channel();
channel.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

다음 예제도 비슷하지만 이번에는 ChannelPipeline을 통해 기록한다.

ChannelHandlerContext ctx = ...;
ChannelPipeline pipeline = ctx.pipeline();
pipeline.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

Channel 또는 ChannelPipeline에서 호출된 write() 메서드는 파이프라인을 통해 끝까지 전파되는 점은 같지만,

한 핸들러에서 다음 ChannelHandler단계로 전파하는 일은 ChannelHandlerContext가 한다는 점이 다르다.

 

  • ChannelPipeline의 특정 지점에서 이벤트 전파를 시작하는 이유가 뭘까?
    • 관련이 없는 ChannelHandler를 통과하면서 생기는 오버헤드를 줄일 수 있다.
    • 이벤트와 관련된 핸들러에서 이벤트가 처리되는 것을 방지할 수 있다.

 

ChannelHandlerContext의 write() 호출의 예를 보자.

ChannelHandlerContext ctx = ...;
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

 

 

 

 

 

정리

 

 

  • Channel 하나당 ChannelPipeline 1개가 자동 할당된다.
  • Pipeline안에는 여러 ChannelHandler로 구성된다.
  • Channel, ChannelPipeline 메소드와 ChannelHandlerContext 메서드 적용의 차이점
  • Channel, ChannelPipeline, ChannelHandlerContext의 다양한 메서드

 

 

 

 

 

출처 : 네티인 액션

https://book.naver.com/bookdb/book_detail.nhn?bid=10462610

 

네티 인 액션

네티는 복잡한 네트워킹, 멀티스레드, 동시성을 관리하는 자바 기반 네트워킹 프레임워크로서, 반복적인 저수준 코드를 내부로 감춤으로써 비즈니스 논리를 분리하고 쉽게 재사용할 수 있게 해

book.naver.com

 

반응형

'IT' 카테고리의 다른 글

Netty 에코 서버-클라이언트 구현 (feat. 네트워크 소녀 Netty)  (0) 2021.01.07
Netty 코덱  (0) 2021.01.05
Netty ByteBuf - 바이트 버퍼  (0) 2020.12.29
Netty 전송 API  (0) 2020.12.28
Netty 컴포넌트와 설계  (2) 2020.12.10