네티의 데이터 컨테이너
java.NIO.ByteBuffer
라는 자바 바이트 컨테이너를 제공하지만, 사용법이 복잡하여 이를 대신하는 네티는 ByteBuf
클래스를 제공한다.
우선 ByteBuf
의 장점을 간단히 알아보고 가자.
- 사용자 정의 버퍼 형식으로 확장 가능
- 내장 복합 버퍼 형식을 통해 투명한 제로 카피 달성 가능
- 용량을 필요에 따라 확장 가능 -
StringBuilder
처럼 - ByteBuffer()의 filp() 메서드 호출 없이 리더와 라이터 모드의 전환 가능
- 읽기와 쓰기에 고유 인덱스를 적용
- 메서드 체인의 지원
- 참조 카운팅 지원
- 풀링 지원
ByteBuf는 읽기와 쓰기에 관한 두 인덱스를 가지고 있다. (cf. ByteBuffer는 인덱스가 하나만 존재)
readerIndex
- 데이터를 읽으면 바이트 수만큼 증가
writerIndex
- 데이터를 기록하면 바이트 수만큼 증가
ByteBuf의 메서드 중 read 나 write로 시작하는 메서드들은 이 인덱스들을 조절하게 된다. (cf. set, get 은 인덱스를 증가시키지 않는다)
보조 배열(backing array) 이라고 부르는 가장 자주 사용하게 되는 ByteBuf
패턴이다.
이름 그대로 JVM Heap 영역에 저장한다. 또한 풀링이 사용되지 않는 경우 빠른 할당과 해제속도를 보인다.
ByteBuf heapBuf = ...;
if (heapBuf.hasArray()) { // 보조 배열이 있는지 확인
byte[] array = heapBuf.array(); // 있는 경우 참조를 얻음
int offset = heapBuf.arrayOffset() + heapBuf.readerIndex(); // 첫번째 바이트에 대한 오프셋 계산
int length = heapBuf.readableBytes(); // 읽을 수 있는 바이트 수를 얻음
handleArray(array, offset, length); // 메서드 호출
}
주요 특징
- JVM Heap 영역 바깥에 위치한다.
- 네트워크 데이터 전송에 이상적이다
- Heap 영역에 데이터가 있으면, JVM은 소켓을 통해 전송하기 전에 내부적으로 다이렉트 버퍼로 복사해야 한다.
- 힙 기반 버퍼보다 할당과 해제의 비용 부담이 크다
ByteBuf directBuf = ...;
if (!directBuf.hasArray()) {
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(), array);
handleArray(array, offset, length);
}
Tip: 컨테이너의 데이터를 배열로 접근하는 것을 안다면, 다이렉트 버퍼보다 힙 버퍼를 사용하자
말 그대로 여러 버퍼가 병합된 가상의 하나의 버퍼를 말한다. - CompositeByteBuf
복합 버퍼를 사용하는 예)
- 헤더와 본문의 두 부분으로 구성되는 HTTP 메시지
- 각각 다른 애플리케이션에서 생성된 메시지 본문을 조립하여 하나의 메시지로 전송 필요
Netty 사용없이 JDK의 ByteBuffer를 이용하여 구현
ByteBuffer[] message = new ByteBuffer[] { header, body };
ByteBuffer message2 = ByteBuffer.allocate(header.remaining() + body.remaining());
message2.put(header);
message2.put(body);
message2.flip();
Netty의 CompositeByteBuf
를 이용하여 구현
// 복합 버퍼 패턴
CompositeBytebuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...;
ByteBuf body = ...;
messageBuf.addComponents(headerBuf, bodyBuf);
...
messageBuf.removeComponent(0);
for (ByteBuf buf : messageBuf) {
System.out.println(buf.toString());
}
// 데이터 접근
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
int length = compuf.readableBytes();
byte[] array = new byte[length];
compBuf.getBytes(compBuf.readerIndex(), array);
handleArray(array, 0, array.length);
+------------------+----------------+------------------+
| 폐기할 수 있는 바이트 | 읽을 수 있는 바이트 | 기록할 수 있는 바이트 |
+------------------+----------------+------------------+
0 <-------------readerIndex <-----writerIndex <-------capacity
discardReadBytes() 호출 후
+------------------+-----------------------------------+
| 읽을 수 있는 바이트 | 기록할 수 있는 바이트 |
+------------------+-----------------------------------+
readerIndex<------writerIndex <-------------------capacity
-
일반 자바 바이트 배열과 비슷하다
- 0 기반 인덱싱, 마지막 바이트는 capacity - 1
-
discardReadBytes()
- 이미 읽은 바이트를 폐기하고 기록할 수 있는 바이트의 크기를 증가시킨다
- 자주 사용시 부담이 있다. - 읽을 수 있는 바이트를 버퍼의 시작 부분으로 옮기기 위해 메모리 복사가 실행됨
-
read
-
readBytes(ByteBuf dest);
-
ByteBuf buffer = ...; while (buffer.isReadable()) { System.out.println(buffer.readByte()); }
-
읽을 수 있는 바이트를 다 읽은 뒤 읽으려고 하면
IndexOutOfBoundsException
-
-
write
-
writeBytes(ByteBuf dest);
-
ByteBuf buffer = ...; while (buffer.writeableBytes() >= 4) { buffer.writeInt(random.nextInt()); }
-
duplicate()
slice()
slice(int, int)
Unpooled.unmodifiableBuffer(...)
order(ByteOrder)
readSlice(int)
- 실제 데이터 페이로드와 함께 다양한 속성 값을 저장해야하는 경우에 사용 ex) HTTP의 상태 코드, 쿠키 등
ByteBufAllocator 인터페이스를 이용하여 ByteBuf 인스턴스를 할당하는 데 이용할 수 있는 풀링을 구현한다.
// ByteBufAllocator 참조 얻기
Channel channel = ...;
ByteBufAllocator allocator = channel.alloc();
....
ChannelHandlerContext ctx = ...;
ByteBufAllocator allocator2 = ctx.alloc();
Unpooled
라는 유틸리티 클래스를 제공하여 풀링되지 않는 ByteBuf 인스턴스 생성을 도와준다.
ByteBuf를 조작하기 위한 정적 도우미 메서드를 제공한다.
그 중에 hexdump()
라는 메서드는 ByteBuf의 내용을 16진수 표현으로 출력하게 해준다. - 로깅에 유용
-
다른 객체에서 더 이상 참조하지 않는 객체가 보유한 리소스를 해제해 메모리 사용량과 성능을 최적화하는 기법이다.
-
Netty 4버전 부터
ReferenceCounted
인터페이스를 이용하여 Byte 와 ByteBufHodler에 참조 카운팅을 도입했다. -
PooledByteBufAllocator 와 같은 풀링 구현에서 메모리 할당의 오버헤드를 줄이는 데 반드시 필요하다.