Language/Java

Netty Codec Framework

park_juyoung 2018. 12. 15. 22:32

코덱이란?

모든 어플리케이션은 네트워크 상에 원시 바이트를 주고 받는다. 바이트 형태의 데이터를 대상 어플리케이션에 맞는 데이터 포맷으로 분석하고 변환하는 것이 필요하다. 이러한 데이터 변환은 인코더와 디코더로 구성된 코덱에 의해 처리된다.

일반적으로 동영상 압축 알고리즘을 코덱이라고 부른다. 예를 들면 MPEG 알고리즘으로 압축된 동영상을 재생한다면, 원본 동영상을 MPEG 알고리즘으로 압축하고 시청자는 다시 MPEG 디코더로 압축 해제한 뒤 시청을 할 것 이다.

디코더(Decoder)

디코더는 네트워크 스트림을 프로그램의 메세지 포맷으로 변환한다. 즉 인바운드 데이터를 처리한다. 디코더는 인바운드 데이터를 다른 포맷으로 변환하는 일을 하므로 ChannelInboundHandler를 상속받는다.


Netty의 디코더 클래스

  • ByteToMessageDecoder / ReplayingDecoder : 바이트 스트림을 메세지로 디코딩
  • MessageToMessageDecoder : 메세지를 다른 메세지 유형으로 디코딩

메세지란 ?

특정 어플리케이션에서 의미가 있는 바이트의 시퀀스 구조를 메세지라고 한다. 인코더의 경우 이 메세지를 전송하기에 적합한 (보통 바이트 스트림) 형식으로 변환하고, 디코더는 네트워크 스트림을 다시 프로그램의 메세지 포맷으로 변환한다

디코더는 인바운드 데이터를 ChannelPipeline의 다음 ChannelInboundHandler를 위해 변환할 때 이용한다. ChannelPipeline은 설계 방식이 체인 형태이기 때문에 복잡한 논리도 여러 디코더를 체인으로 연결해서 쉽게 구현할 수 있다.


ByteToMessageDecoder

바이트 스트림을 메세지(또는 다른 바이트의 시퀀스)로 디코딩 하는 일반적인 작업을 지원하는 추상 기본 클래스. 원격 피어가 완성된 메세지를 한번에 보낼지 알 수 없으므로 이 클래스는 인바운드 데이터가 처리할 만큼 모일 때까지 버퍼에 저장한다.

  • ByteToMessageDecoder API
메소드설명
decode(ChannelHandlerContext ctx, ByteBuf in, List out)decode()는 구현해야하는 유일한 추상 메소드로 들어오는 데이터가 포함된 ByteBuf와 디코딩된 메세지가 추가될 List를 받는다. 이 호출은 더 이상 List에 추가할 항목이 없거나 ByteBuf에 읽을 바이트가 없을 때까지 반복된다. 그 이후 List가 비어있지 않은 경우 그 내용이 파이프라인의 다음 핸들러로 전달된다.
decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out)네티가 제공하는 기본 구현은 단순히 decode()를 호출한다. 이 메소드는 Channel이 비활성화될 때 한번 호출된다. 특별한 처리가 필요한 경우 이 메소드를 재정의한다.
  • ByteToMessageDecoder를 상속받은 ToIntegerDecoder 클래스
public class ToIntegerDecoder extends ByteToMessageDecoder {
  // 바이트를 특정 포맷으로 디코딩하기 위해서 ByteToMessageDecoder를 상속받음
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {
      if(in.readableBytes() >= 4){
        // 최소 4바이트(int 길이)를 읽을 수 있는지 확인
        out.add(in.readInt());
        // 인바운드 ByteBuf에서 int 하나를 읽고 이를 디코딩된 메세지의 List에 추가
      }
    }
}

int를 포함하는 바이트 스트림을 받고 각각 별도로 처리하는 경우, 인바운드 ByteBuf에서 각 int를 읽고 이를 파이프라인의 다음 ChannelInboundHandler로 전달해야한다. 위의 예제는 인바운드 ByteBuf에서 4바이트씩 읽고 이를 int로 디코딩한 후 List로 추가한다. 그리고 List에 추가할 항목이 더 이상 없는 경우 그 내용을 다음 ChannelInboundHandler로 전달한다. 위의 예제는 readableBytes 메소드로 ByteBuf에 읽을 데이터가 있는지 확인하는 과정이 필요하다.


ReplayingDecoder

public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder

ReplayingDecoder는 ByteToMessageDecoder를 상속받으며 위의 예제와 다르게 약간의 오버헤드를 감수하고 readableBytes를 호출할 필요를 없애준다. 이를 위해 들어오는 ByteBuf를 커스텀 ByteBuf 구현인 ReplayingDecoderBuffer로 래핑하는데 이 동작은 내부적으로 호출이 수행된다.

  • ReplayingDecoder를 상속받은 ToIntegerDecoder 클래스
public class ToIntegerDecoder extends ReplayingDecoder<Void> {
  // 바이트 스트림을 메세지로 디코딩하기 위해 ReplayingDecoder<Void>를 상속받음
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
  // 들어오는 ByteBuf는 ReplayingDecoderBuffer이다.
    List<Object> out) throws Exception {
      out.add(in.readInt());
      // 인바운드 ByteBuf에서 int 하나를 읽고 이를 디코딩된 메세지의 List에 추가함
    }
}
  • ReplayingDecoder의 특징
    • 모든 ByteBuf 작업이 지원되는 것은 아니다. 지원되지 않는 메소드를 호출하면UnsupportedOperationException이 발생한다.
    • ReplayingDecoder는 ByteToMessageDecoder보다 약간 느리다


MessageToMessageDecoder

public abstract class MessageToMessageDecoder<I>
  extends ChannelInboundHandlerAdapter

메세지 포맷을 변환하는 클래스이다. 예를 들면 POJO의 한 형식에서 다른 형식으로 바꾸는 등에 이용할 수 있다. 위의 형태에서 매개변수 I는 구현해야하는 유일한 메소드인 decode()에 입력 msg 인수의 형식을 알려준다.

  • MessageToMessageDecoder API
메소드설명
decode(ChannelHandlerContext ctx, I msg, List out)인바운드 메세지를 다른 포맷으로 디코딩할 때마다 호출된다. 디코딩된 메세지는 파이프라인의 다음 ChannelInboundHandler로 전달된다.
  • IntegerToStringDecoder 클래스
public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
  @Override
  public void decode(ChannelHandlerContext ctx, Integer msg,
    List<Object> out) throws Exception {
      out.add(String.valueOf(msg));
      //Integer 메세지를 String 표현으로 변환한 후 출력 List에 추가
    }
}


TooLongFrameException

네티는 비동기 프레임워크이므로 디코딩할 수 있을 때까지 바이트를 메모리 버퍼에 저장해야한다. 또한 디코더가 메모리를 소진할 만큼 많은 데이터를 저장해서는 안된다. 이 문제를 해결하기 위해서 프레임이 지정한 크기를 초과하면 발생하는 TooLongFrameException 예외를 제공한다.

이 예외는 ChannelHandler.exceptionCaught()로 포착할 수 있다. 예외를 처리하는 방법은 디코더의 이용자가 결정할 수 있다. HTTP 같은 특정 프로토콜을 이용할 때는 특수한 응답을 반환할 수 있지만 그 밖의 경우에는 연결을 닫는 것이 유일한 방법이다.

  • TooLongFrameException 예제
public class SafeByteToMessageDecoder extends ByteToMessageDecoder {
  // 인바운드 바이트를 메세지로 디코딩하기 위해 ByteToMessageDecoder를 상속받음
  private static final int MAX_FRAME_SIZE = 1024;
  // 최대 바이트 수의 임계값을 설정한다.
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {
      int readable = in.readableBytes();
      if (readable > MAX_FRAME_SIZE){
        // 버퍼의 바이트 수가 MAX_FRAME_SIZE를 초과하는 지 확인
        // 읽을 수 있는 바이트를 모두 건너뛰고
        // TooLongFrameException을 생성한 후
        // 다른 ChannelHandler에 알림
        in.skipBytes(readable);
        throw new TooLongFrameException("Frame Too big");
      }
      // 필요한 작업을 수행
      ...
    }
}

ByteToMessageDecoder가 TooLongFrameException을 이용해 ChannelPipeline 내의 다른 ChannelHandler에 프레임 초과를 알리는 코드이다. 이러한 예방책은 프레임 크기가 가변적인 프로토콜을 이용할 때 특히 중요하다.


인코더(Encoder)

인코더는 아웃바운드 데이터를 한 포맷에서 다른 포맷으로 변환한다. 즉 아웃바운드 데이터를 처리하므로 ChannelOutboundHandler를 상속받는다.

  • Netty의 인코더 클래스
    • MessageToByteEncoder : 메세지를 바이트로 인코딩
    • MessageToMessageDecoder : 메세지를 다른 메세지로 인코딩


MessageToByteEncoder

앞의 ByteToMessageDecoder와 반대로 메세지를 바이트로 변환하는 클래스이다.

  • MessageToByteEncoder API
메소드설명
encode(ChannelHandlerContext ctx, I msg, ByteBuf out)encode 메소드는 구현해야하는 유일한 추상 메소드이다. ByteBuf로 인코딩할 아웃바운드 메세지(I 형식)을 전달하고 호출한다. 그런 다음 ByteBuf는 파이프라인의 다음 ChannelOutboundHandler로 전달된다.

디코더에는 메소드가 두개이지만 인코더인 이 클래스는 메소드가 하나이다. 디코더의 메소드가 두 개인 이유는 Channel이 닫힌 후 마지막 메세지를 생성해야하는 일이 자주 있기 때문이다.(decodeLast() 메소드). 인코더는 연결이 닫힌 후 메세지를 생성할 필요가 없기 때문에 메소드가 하나이다.

  • ShortToByteEncoder 클래스
public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
  @Override
  public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
    throws Exception {
      out.writeShort(msg);
      // Short를 ByteBuf에 기록함
    }
}

Short 인스턴스를 메세지로 받고 이를 Short 기본형으로 인코딩한 후 ByteBuf에 저장하고 이를 파이프라인 내의 다음 ChannelOutboundHandler로 전달한다. 나가는 Short는 ByteBuf에서 2바이트를 차지한다.


MessageToMessageEncoder

메세지를 아웃바운드 데이터로 인코딩하는 클래스이다.

  • MessageToMessageEncoder API
이름설명
encode(ChannelHandlerContext ctx, I msg, List out)encode()는 구현해야하는 유일한 메소드이다. write()로 기록한 각 메세지는 encode()로 전달된 후 하나 이상의 아웃바운드 메세지로 인코딩된다. 그런다음 파이프라인의 다음 ChannelOutboundHandler로 전달된다.
  • IntegerToStringEncoder 클래스
public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer> {
  @Override
  public void encode(ChannelHandlerContext ctx, Integer msg,
    List<Object> out) throws Exception {
    out.add(String.valueOf(msg));
    // Integer를 String으로 변환하고 List에 추가
  }
}


추상 코덱 클래스

지금까지 인코더와 디코더를 별개로 다뤘지만 인바운드/아웃바운드 데이터와 메세지 변환을 한 클래스에서 관리할 수 있다. 네티의 추상 코덱 클래스는 디코더와 인코더의 작업을 함께 처리할 수 있다. 이 클래스는 ChannelInboundHandler와 ChannelOutboundHandler를 둘 다 구현한다. 이렇게 구현하는 경우 코드의 재사용성과 확장성은 떨어진다.


ByteToMessageCodec

바이트를 일종의 메세지(ex-POJO)로 디코딩한 후 다시 인코딩해야하는 경우, ByteToMessageDecoder와 반대 작업을 하는 MessageToByteEncoder를 결합하는 ByteToMessageCodec을 이용하면 된다.

  • ByteToMessageCodec API
메소드설명
decode(ChannelHanlder ctx, ByteBuf in, List out)이 메소드는 읽을 바이트가 있을 때 호출되고, 인바운드 ByteBuf를 지정한 메세지 포맷으로 변환하고 파이프라인 내의 다음 ChannelInboundHandler로 전달한다.
decodeLast(ChannelHandlerContext ctx, ByteBuf in, List out)이 메소드의 기본 구현은 decode()로 위임하는 것이다. 이 메소드는 Channel이 비활성화될 때 한번 호출된다. 특별한 처리가 필요한 경우 이 메소드를 재정의한다.
encode(ChannelHanlderContext ctx, I msg, ByteBuf out)이 메소드를 I 형식의 각 메소드를 인코딩하고 아웃바운드 ByteBuf에 기록한다.


MessageToMessageCodec

public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN>
  • MessageToMessageCodec의 메소드
메소드설명
protected abstract decode(ChannelHandlerContext ctx, INBOUND_IN msg, List out)이 메소드는 INBOUND_IN 형식의 메세지를 받으면 이를 OUTBOUND_IN 형식의 메세지로 디코딩한다. 메세지는 ChannelPipeline 내의 다음 ChannelInboundHandler로 전달된다.
protected abstract encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, Listout)이 메소드는 OUTBOUND_IN 형식의 각 메세지를 처리할 때마다 호출한다. 처리된 메세지는 INBOUND_IN 형식의 메세지로 인코딩된 후 파이프라인 내의 다음 ChannelOutboundHandler로 전달된다.

INBOUND_IN 메세지는 전송을 위한 형식이고 OUTBOUND_IN 메세지는 어플리케이션에서 처리하는 형식으로 생각하면 된다. 해당 코덱은 서로 다른 메세징 API 간에 메세지를 변환하는 경우 사용된다. 이런 패턴은 레거시 메세지 포맷이나 특정 기업의 메세지 포맷을 이용하는 API와 상호운용해야하는 경우에 사용된다.


CombinedChannelDuplexHandler

디코더와 인코더를 결합해 사용하면 재사용성이 떨어지지만 CombinedChannelDuplexHandler를 이용하면 디코더와 인코더를 단일 유닛으로 배포하면서 재사용성 저하를 방지할 수 있다.

public class CombinedChannelDuplexHandler
  <I extends ChannelInboundHandler,
  O extends ChannelOutboundHandler>

이 클래스는 ChannelInboundHandler와 ChannelOutboundHandler의 컨테이너 역할을 한다.

  • ByteToCharDecoder 클래스
public class ByteToCharDecoder extends ByteToMessageDecoder{
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {
    while(in.readableBytes() >= 2){
        out.add(in.readChar());
        // 나가는 List에 Character 객체를 한 개 이상 추가
    }
  }
}

위의 decode()는 ByteBuf에서 한번에 2바이트씩 읽고 Character 객체로 자동 박싱되는 char로 List에 기록한다.

  • CharToByteEncoder 클래스
public class CharToByteEncoder extends MessageToByteEncoder<Character>{
  @Override
  public void encode(ChannelHandlerContext ctx, Character msg,
    ByteBuf out) throws Exception {
      out.writeChar(msg);
      // Character를 char로 디코딩하고 아웃바운드 ByteBuf로 기록
    }
}

CharToByteEncoder는 Character를 다시 바이트로 변환한다. 이 클래스는 char 메세지를 ByteBuf로 인코딩해야하므로 MessageToByteEncoder를 상속받으며, ByteBuf에 직접 기록하는 방법으로 인코딩한다.

  • CombinedChannelDuplexHandler<I,O>
public class CombinedByteCharCodec extends
  CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
  public CombinedByteCharCodec() {
    super(new ByteToCharDecoder(), new CharToByteEncoder());
    // 인스턴스를 부모로 전달
  }
}

위의 인코더와 디코더를 이용해 결합해 코덱을 만들 수 있다.

이처럼 코덱 클래스 중 하나를 이용해 구현하거나 구현한 코덱을 결합할 수도 있다.




'Language > Java' 카테고리의 다른 글

JDBC- MariaDB와 Java연동  (0) 2019.01.02
Netty 프로젝트 시작하기-server  (0) 2018.12.16
Netty 특징과 아키텍처  (0) 2018.12.16
TCP/IP 소켓 통신이란?  (3) 2018.12.15
HashMap과 HashTable 차이  (0) 2018.12.14