Java NIO ByteBuffer

자바 NIO 바이트 버퍼는 바이트 데이터를 저장하고 읽는 저장소다. 배열을 멤버 변수로 가지고 배열에 대한 읽고 쓰기 메소드를 제공한다. xxxBuffer 형태의 각 데이터별로 버퍼를 제공한다. ByteBuffer는 capacity / position / limit 세가지 속성을 가진다.

  • capacity : 버퍼에 저장할 수 있는 데이터의 최대 크기. 한번 정하면 바꿀 수 없다. 버퍼를 생성할 때 생성자의 인수로 입력된 값이다.
  • position : 읽기 또는 쓰기 작업 중인 위치를 나타낸다. 버퍼 객체가 생성되면 0으로 초기화되고 쓰기(put) 또는 읽기(get) 작업이 수행되면 자동으로 증가한다.
  • limit : 읽고 쓸 수 있는 버퍼 공간의 최대치. limit 메소드로 값을 조절할 수 있지만 capacity보다 크게 설정할 수 없다.


Java ByteBuffer 생성 및 종류

자바의 ByteBuffer는 생성자가 아닌 추상 클래스의 메소드를 통해 생성한다. 아래의 메소드를 이용한다.

  • allocate : JVM 힙 영역에 바이트 버퍼를 생성. 이를 보통 힙 버퍼라고 한다. 인수는 앞에서 설명한 capacity 값에 해당하는 버퍼의 크기이다. 바이트 버퍼의 값은 모두 0으로 초기화된다. 힙 버퍼는 풀링이 사용되지 않는 경우 빠른 할당과 해제 속도를 보여준다.
  • allocateDirect : JVM 힙 영역이 아닌 OS의 커널 영역에 바이트 버퍼를 생성한다. 이를 다이렉트 버퍼라고 한다. allocateDirect 메소드는 ByteBuffer 추상 클래스만 사용할 수 있다. 즉 Direct Buffer는 ByteBuffer로만 생성할 수 있다. 다이렉트 버퍼는 힙 버퍼에 비해 생성 시간은 길지만 더 빠른 IO 성능을 제공한다.

    전송할 데이터가 힙에 할당된 버퍼에 있는 경우 JVM은 소켓을 통해 데이터를 전송하기 전에 내부적으로 버퍼를 다이렉트 버퍼로 복사한다. 다이렉트 버퍼를 사용하면 이런 오버헤드를 줄일 수 있다. 하지만 다이렉트 버퍼의 데이터에 접근하려면 복사본을 만들어야 접근할 수 있다는 단점이 있다.

  • wrap : 입력된 바이트 배열을 이용해 바이트 버퍼를 생성한다. 입력에 사용된 바이트 배열이 변경되면 wrap을 사용해 생성한 바이트 배열의 값도 변경된다.


그외 Java NIO ByteBuffer의 특징

  • Java ByteBuffer는 읽기 / 쓰기 작업시 같은 position의 값이 바뀐다. 읽기 / 쓰기 인덱스가 분리되어 있지 않아서 작업 전환시 flip() 메소드를 사용해야한다. 그리고 다중 스레드 환경에서 바이트 버퍼를 공유하지 않아야한다.


Netty Architecture Overview


Netty ByteBuf 특징

  • heapBuf

  • directBuf

  • 별도의 Read Index / Write Index가 있다.
  • 위의 이유로 flip() 메소드를 사용하지 않아도 된다.
  • 가변 바이트 버퍼를 사용할 수 있다.
  • 바이트 버퍼 풀 기능을 제공한다.
  • 복합 버퍼 사용이 가능하다.(Heap + Direct)
  • Java의 ByteBuffer와 Netty의 ByteBuf의 상호 변환이 가능하다.


Netty의 ByteBuf 생성

네티의 바이트 버퍼는 자바의 바이트 버퍼와 다르게 프레임워크 레벨의 바이트 버퍼 풀을 제공하고 이를 통해 생성된 바이트 버퍼를 재사용한다. Netty의 바이트 버퍼를 바이트 버퍼 풀에 할당하려면 ByteBufAllocator 인터페이스를 사용한다. ByteBufAllocator의 하위 추상 구현체인 PooledByteBufAllocator 클래스로 각 바이트 버퍼를 생성한다.


Pooled / Unpooled ByteBuf

자바의 바이트 버퍼는 데이터 형에 따른 ByteBuffer 생성을 지원했지만 네티는 풀링 여부로 ByteBuf를 구분한다. Unpooled 클래스와 PooledByteBufAllocator 사용해 생성하고 다이렉트 버퍼와 힙 버퍼를 생성하기 위해 directBuffer 메소드와 heapBuffer 메소드를 사용한다.

ByteBuf 종류풀링 함풀링 안 함
힙 버퍼PooledHeapByteBufUnpooledHeapByteBuf
다이렉트 버퍼PooledDirectByteBufUnpooledDirectByteBuf

PooledByteBufAllocator는 ByteBuf 인스턴스를 풀링해 성능을 개선하고 메모리 단편화를 최소화한다. UnpooledByteBufAllocator는 ByteBuf 인스턴스를 풀링하지 않고 호출될 때마다 새로운 인스턴스를 반환한다.

생성 방법풀링 함풀링 안 함
힙 버퍼ByteBufAllocator.DEFAULT.heapBuffer()Unpooled.buffer()
다이렉트 버퍼ByteBufAllocator.DEFAULT.directBuffer()Unpooled.directBuffer()
  • ByteBuf 생성 예
ByteBuf buf = Unpooled.buffer(11);
// 바이트 버퍼 풀을 사용하지 않는 11바이트 크기의 힙 버퍼 생성
ByteBuf buf = Unpooled.directBuffer(11);
// 바이트 버퍼 풀을 사용하지 않는 11바이트 크기의 다이렉트 버퍼를 생성
ByteBuf buf = PooledByteBufAllocator.DEFAULT.heapBuffer(11);
// 풀링된 11바이트 크기의 힙 버퍼 생성
ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(11);
// 풀링된 11바이트 크기의 다이렉트 버퍼 생성

크기를 지정하지 않으면 Netty에 지정된 기본 값인 256바이트 크기의 바이트 버퍼가 생성된다.


Read / Write

ByteBuf의 읽을 수 있는 바이트 세그먼트에 실제 데이터가 저장된다. 이때 새로 할당, 래핑, 복사된 버퍼에서 readerIndex의 기본값은 0이다. read나 skip으로 시작하는 모든 메소드는 현재 readerIndex 위치에 있는 데이터를 읽거나 건너 뛰고 읽은 바이트 수 만큼 readerIndex를 증가시킨다.

ByteBuf의 기록할 수 있는 바이트 세그먼트는 정의되지 않은 내용이 들어 있고 기록할 수 있는 영역이다. 새로 할당된 버퍼의 writerIndex 기본값은 0이고 write로 시작하는 모든 메소드는 현재 writerIndex 위치부터 데이터를 기록하고 기록한 만큼 writerIndex를 증가시킨다.

readBytes(ByteBuf dest)인수의 ByteBuf만큼 읽는다
writeBytes(ByteBuf dest)인수의 ByteBuf만큼 쓴다.
  • 모든 데이터 읽기
    ByteBuf buffer = ...;
    while(buffer.isReadable()){
    System.out.println(buffer.readByte());
    }
    
  • 데이터 기록
    // 버퍼의 기록할 수 있는 바이트를 임의의 정수로 채움
    ByteBuf buffer = ...;
    while(buffer.writableBytes() >= 4) {
    buffer.writeInt(random.nextInt());
    }
    

읽기 / 쓰기 작업

Netty ByteBuf의 읽기, 쓰기 작업은 두가지로 나뉜다.

  • get()set() 작업은 저장한 인덱스에서 시작하고 인덱스를 변경하지 않는다.
  • read()write() 작업은 지정한 인덱스에서 시작하고 접근한 바이트 수만큼 인덱스를 증가시킨다.

자주 쓰이는 get() 작업

이름설명
getBoolean(int)지정한 인덱스의 Boolean 값을 반환
getByte(int)지정한 인덱스의 바이트를 반환
getUnsignedByte(int)지정한 인덱스의 부호 없는 바이트 값을 short로 반환
getMedium(int)지정한 인덱스의 24비트 미디엄 int값을 반환
getUnsignedMedium(int)지정한 인덱스의 부호 없는 24비트 미디엄 int 값을 반환
getInt(int)지정한 인덱스의 int값을 반환
getUnsignedInt(int)지정한 인덱스의 부호 없는 int값을 long으로 반환
getLong(int)지정한 인덱스의 long 값을 반환

자주 쓰이는 set() 작업

이름설명
setBoolean(int, boolean)지정한 인덱스의 Boolean 값을 설정
setByte(int index, int value)지정한 인덱스의 바이트 값을 설정
setMedium(int index, int value)지정한 인덱스의 24비트 미디엄 값을 설정
setInt(int index, int value)지정한 인덱스의 int 값을 설정
setLong(int index, long value)지정한 인덱스의 long 값을 설정
setShort(int index, int value)지정한 인덱스의 short 값을 설정
  • get()/set() 예제
    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty !", utf8);
    System.out.println((char)buf.getByte(0));
    // 첫번째 문자 'N' 출력
    int readerIndex = buf.readerIndex();
    int writerIndex = buf.writerIndex();
    // 현재 인덱스들을 저장
    buf.setByte(0, (byte)'B');
    // 인덱스 0에 있는 바이트를 문자'B'로 변경
    System.out.println((char)buf.getByte(0));
    // 첫번째 문자 'B'를 출력
    assert readerIndex = buf.readerIndex();
    assert writerIndex = buf.writerIndex();
    // 인덱스가 바뀌지 않았으므로 성공
    

자주 쓰이는 read() 작업

이름설명
readBoolean()현재 readerIndex 위치의 Boolean 값을 반환하고 readerIndex를 1만큼 증가시킨다
readByte()현재 readerIndex 위치의 바이트 값을 반환하고 readerIndex를 1만큼 증가시킨다
readUnsignedByte()현재 readerIndex 위치의 short 값을 반환하고 readerIndex를 1만큼 증가시킨다
readMedium()현재 readerIndex 위치의 24비트 미디엄 값을 반환하고 readerIndex를 3만큼 증가시킨다
readUnsignedMedium()현재 readerIndex 위치의 부호 없는 24비트 미디엄 값을 반환하고 readerIndex를 3만큼 증가시킨다
readInt()현재 readerIndex 위치의 int값을 반환하고 readerIndex를 4만큼 증가시킨다
readUnsignedInt()현재 readerIndex 위치의 부호 없는 int값을 long으로 반환하고 readerIndex를 4만큼 증가시킨다
readLong()현재 readerIndex 위치의 long 값을 반환하고 readerIndex를 8만큼 증가시킨다
readShort()현재 readerIndex 위치의 short 값을 반환하고 readerIndex를 2만큼 증가시킨다
readUnsignedShort()현재 readerIndex 위치의 부호 없는 short값을 int로 반환하고 readerIndex를 2만큼 증가시킨다
readBytes(ByteBuf | byte[] destination, int dstIndex [,int length])현재 ByteBuf의 현재 readerIndex로부터 시작하는 바이트를 (length가 지정된 경우 length 바이트 만큼) 대상 ByteBuf또는 byte[]의 대상 dstIndex로부터 전송한다. 로컬 readerIndex는 전송된 바이트 수만큼 증가한다.

자주 쓰이는 write() 작업

이름설명
writeBoolean(boolean)현재 writerIndex 위치에 Boolean값을 기록하고 writerIndex를 1만큼 증가시킨다
writeByte(int)현재 writerIndex 위치에 바이트 값을 기록하고 writerIndex를 1만큼 증가시킨다
writeMedium(int)현재 writerIndex 위치에 미디엄 값을 기록하고 writerIndex를 3만큼 증가시킨다
writeInt(int)현재 writerIndex 위치에 int 값을 기록하고 writerIndex를 4만큼 증가시킨다
writeLong(long)현재 writerIndex 위치에 long값을 기록하고 writerIndex를 8만큼 증가시킨다
writeShort(int)현재 writerIndex 위치에 short값을 기록하고 writerIndex를 2만큼 증가시킨다
writeBytes(source ByteBuf | byte[] [, int srcIndex, int length])지정된 원본(ByteBuf 또는 byte[])의 현재 writerIndex부터 데이터 전송을 시작한다. srcIndex와 length가 지정된 경우 srcIndex부터 시작해 length 바이트 만큼 읽는다. 현재 writerIndex는 기록된 바이트 수만큼 증가한다
  • read()/write() 예제
    Charset utf8 = Charset.forName("UTF-8");
    ByteBuf buf = Unpooled.copiedBuffer("Netty !", utf8);
    // 지정한 문자열의 바이트를 저장하는 ByteBuf 생성
    System.out.println((char)buf.readByte());
    // 첫번째 문자 'N'을 출력
    int readerIndex = buf.readerIndex();
    int writerIndex = buf.writerIndex();
    // 현재 readerIndex/writerIndex 를 저장
    buf.writeByte((byte)'?');
    // 버퍼에 '?'를 추가
    assert readerIndex == buf.readerIndex();
    assert writerIndex != buf.writerIndex();
    

기타 유용한 메소드

이름설명
isReadable()읽을 수 있는 바이트가 하나 이상이면 true를 반환
isWritable()기록할 수 있는 바이트가 하나 이상이면 true를 반환
readableBytes()읽을 수 있는 바이트 수를 반환
writableBytes()기록할 수 있는 바이트 수를 반환
capacity()ByteBuf가 저장할 수 있는 바이트 수를 반환한다. 이 수를 초과하면 maxCapacity()에 도달할 때까지 용량이 확장된다.
maxCapacity()ByteBuf가 저장할 수 있는 최대 바이트 수를 반환
hasArray()ByteBuf에 힙 버퍼가 있는 경우 true를 반환
array()ByteBuf에 힙 버퍼가 있는 경우 해당 바이트 배열을 반환하며, 그렇지 않으면 UnsupportedOperationException을 발생시킨다.


ByteBufAllocator

자바 바이트 버퍼는 언어 자체에서 제공하는 버퍼 풀이 없다. 따라서 바이트 버퍼 풀을 이용하려면 객체 풀링을 제공하는 서드파티 라이브러리를 사용하거나 직접 구현해야한다. 네티는 프레임 워크에서 바이트 버퍼 풀을 제공하고 있으며 다이렉트 버퍼와 힙 버퍼를 모두 풀링할 수 있다. 네티의 바이트 버퍼 풀링은 ByteBufAllocator를 사용해 바이트 버퍼를 생성할 때 자동으로 수행된다.

ByteBufAllocator의 참조는 Channel에서 얻거나 ChannelHandler에 바인딩 된 ChannelHandlerContext를 통해 얻을 수 있다.

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc()
// Channel에서 ByteBufAllocator를 얻음
...
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
// ChannelHandlerContext에서 ByteBufAllocator를 얻음
...
ByteBuf newBuffer = ByteBufAllocator.buffer();
// ByteBufAllocator의 buffer 메소드를 사용해 생성된 바이트 버퍼
// 똑같이 ByteBufAllocator에서 관리된다.
// 바이트 버퍼를 채널에 기록하거나 명시적으로 release 호출 시 바이트 버퍼 풀로 돌아간다.
...
// new Buffer 사용
ctx.write(msg);
// write 메소드 인수로 바이트 버퍼가 입력되면 데이터를 채널에 기록하고 난 뒤 버퍼 풀로 돌아간다.

풀링되지 않은 버퍼

ByteBufAllocator의 참조가 없는 경우, 네티는 풀링되지 않는 ByteBuf 인스턴스를 생성하는 정적 도우미 메서드 Unpooled 클래스를 제공한다.

이름설명
buffer풀링되지 않은 힙 기반 ByteBuf 반환
directBuffer풀링되지 않은 다이렉트 ByteBuf 반환
wrappedBuffer지정한 데이터를 래핑하는 ByteBuf 반환
copiedBuffer지정한 데이터를 복사하는 ByteBuf 반환

Unpooled 클래스는 다른 네티 컴포넌트가 필요 없는 네트워킹과 무관한 프로젝트에 ByteBuf를 제공해 확정성 높은 고성능 버퍼 API를 이용할 수 있게 해준다.

참조 카운팅

네티는 바이트 버퍼를 풀링하기 위해 바이트 버퍼에 참조수를 기록한다. ReferenceCountUtil클래스에 정의된 retain 메소드와 release 메소드를 사용할 수 있다 retain 메소드는 참조 수를 증가시키고 release 메소드는 참조 수를 감소 시키고 할당된 메모리가 해제 된다.

Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
// channel에서 ByteBufAllocator를 얻음
....
ByteBuf buffer = allocator.directBuffer();
// ByteBufAllocator로부터 ByteBuf를 할당
assert buffer.refCnt() = 1;
// 참조 카운트가 1인지 확인
...
boolean released = buffer.release();
// 객체에 대한 참조 카운트를 감소 시킴
// 참조 카운트가 0이 되면 객체가 해제되고 메소드가 true를 반환한다.

참조 카운트가 0일 때 release()를 호출하면 IllegalReferenceCountException이 발생한다. 참조 해제는 각 객체가 새로 정의할 수 있다. 예를 들면 클래스의 release() 구현에서 참조 카운트를 현재 값과 상관 없이 0으로 설정하면 모든 활성 참조를 일시에 해제할 수 있다.


기타 작업

부호 없는 값 읽기

자바는 부호 없는 데이터 형이 없어서 네티에서는 부호 없는 데이터를 처리하기 위한 메소드를 제공한다. 네티에서는 부호 없는 데이터를 읽을 때 읽을 데이터보다 큰 데이터 형에 할당한다.

ByteBuf = Unpooled.buffer(11);
buf.writeShort(-1);
// 빈 바이트 버퍼에 음수 -1 기록 (2byte)

assertEquals(65535, buf.getUnsignedShort(0));
// -1은 16진수 표기법으로 0xFFFF이고 이를 부호 없는 정수로 표현하면 65535가 된다.
// 이를 4byte로 저장한다.
// getUnsignedShort 메소드로 바이트 버퍼에 저장된 0번째 바이트부터 2바이트 읽어서
// 4바이트 데이터인 int로 읽어들이면 65535가 된다.
  • 부호 없는 데이터 지원 메소드
메소드원본 데이터형리턴 데이터형
getUnsignedBytebyteshort
getUnsignedShortshortint
getUnsignedMediummediumint
getUnsignedIntintlong

엔디안 변환

네티의 바이트 버퍼의 기본 엔디안은 자바와 동일하게 빅엔디안이다. 리틀 엔디안 바이트 버퍼가 필요한 경우를 order 메소드로 엔디안을 변환할 수 있다.

ByteBuf buf = Unpooled.buffer();
...
ByteBuf lettleEndianBuf = buf.order(ByteOrder.LITTLE_ENDIAN);

order 메소드로 생성한 바이트 버퍼는 새로운 바이트 버퍼가 아닌 주어진 바이트 버퍼의 내용을 공유하는 파생 버퍼이다. 기존 바이트 버퍼의 배열과 인덱스들을 공유한다. 즉 내용은 같지만 리틀 엔디안으로 접근하는 바이트 버퍼를 생성한다.

  • 참고 서적
  • 네티 인 액션
  • 자바 네트워크 소녀 Netty


+ Recent posts