Netty

[Netty] 네티 바이트 버퍼

88dldl 2025. 3. 27. 18:50

자바 네트워크 소녀 네티( 정경석 저 / 이희승 감수, 한빛미디어 ) 를 읽고 정리합니다. 

 

 

📘2부 6장. 네티 상세 -  바이트 버퍼

🔹자바 NIO 바이트 버퍼

바이트 데이터를 저장하고 읽는 저장소다.

배열을 멤버변수로 가지고 있으며 배열에 대한 읽고 쓰기를 추상화한 메서드를 제공한다.

 

자바에서 제공하는 버퍼로는 ByteBuffer, CharBuffer, IntBuffer 등이 있고, 각 바이트 버퍼는 저장되는 데이터형에 따라 적당한 클래스를 선택하여 사용한다.

 

바이트 버퍼 클래스는 내부의 배열 상태를 관리하는 3가지 속성을 가진다.

  • capacity : 버퍼에 저장할 수 있는 최대 데이터 크기. 한번 지정하면 변경 불가
  • position : 읽기 또는 쓰기가 작업 중인 위치
  • limit : 읽고 쓸 수 있는 버퍼 공간의 최대치

 

🔸자바 바이트 버퍼 생성

통상적으로 객체를 생성할 때 생성자를 사용하지만 자바의 바이트 버퍼는 데이터형에 따른 추상 클래스의 팩토리 메서드를 통해 생성한다.

 

바이트 버퍼를 생성하는 메서드는 다음과 같다.

  • allocate : JVM 힙 영역에 바이트 버퍼를 생성한다.
  • allocateDirect : 운영체제의 커널 영역에 바이트 버퍼를 생성한다.
  • wrap : 입력된 바이트 배열을 사용해 바이트 버퍼를 생성한다.

allocate 메서드를 사용해 생성한 버퍼를 힙 버퍼, allocateDirect 메서드를 사용해 생성한 버퍼를 다이렉트 버퍼라고 한다.

다이렉트 버퍼는 힙 버퍼에 비해 생성 시간은 길지만 더 빠른 읽기쓰기 성능을 제공한다.

 

🔸버퍼 사용

put 메서드가 실행될 때 저장된 데이터의 길이만큼 증가한다. get 메서드가 실행될 떄도 읽어들인 길이만큼 증가한다.

@Test
void test() {
    ByteBuffer buffer = ByteBuffer.allocate(11);
    System.out.println("초기 상태 : " + buffer);
    
    buffer.put((byte) 1);
    System.out.println(buffer.get());
    System.out.println(buffer);
}

buffer.put((byte) 1): put 메서드를 사용하여 ByteBuffer에 1을 넣습니다. put()은 position이 가리키는 위치에 데이터를 삽입한 후, position을 1 증가시킨다. 즉, 버퍼의 첫 번째 자리에 1을 넣고 position은 1로 이동한다.

 

System.out.println(buffer.get()): get() 메서드는 현재 position 위치에서 데이터를 읽고, 그 값을 반환한다. 위에서 position이 1로 이동했기 때문에, 처음에 삽입한 값인 1이 출력된다. 또한, position은 1에서 2로 이동한다.

 

+) flip 메서드를 호출하면, limit 속성값이 마지막에 기록한 데이터의 위치로 변경된다.

 

사용하는 인덱스가 하나이기 때문에 읽기 모드에서 쓰기 모드로 전환할 때 주로 사용한다.

 

 

 

🔹네티의 바이트 버퍼

자바의 바이트 버퍼에 비해 더 빠른 성능을 제공한다

 

네티의 바이트 버퍼 풀은 빈번한 바이트 버퍼 할당과 해제에 대한 부담을 줄여주어 가비지 컬렉션에 대한 부담을 줄여준다.

  • 별도의 읽기 인덱스와 쓰기 인덱스
  • flip 메서드 없이 읽기 쓰기 가능
  • 가변 바이트 버퍼
  • 복합 버퍼
  • 바이트 버퍼 풀
  • 자바의 바이트 버퍼와 네티의 바이트 버퍼 상호 변환

네티의 바이트 버퍼는 저장되는 데이터형에 따른 별도의 바이트 버퍼를 제공하지 않는 대신 각 데이터 형에 대한 읽기, 쓰기 메서드를 제공한다.

→ readFloat, writeFloat와 같이 read/write 접미사를 사용한다. 또한 해당 메서드가 호출될때, 읽기/쓰기 인덱스를 증가시킨다.

읽기 인덱스와 쓰기 인덱스가 분리되어있기 때문에 별도의 메서드 호출 없이 읽기와 쓰기를 수행할 수 있다. (flip을 호출할 필요가 없다!)

 

초기 네티 바이트 버퍼 구조

 

 

🔸네티 바이트 버퍼 생성

자바 바이트 버퍼와 달리 프레임워크 레벨의 바이트 버퍼 풀을 제공한다.

=> 바이트 버퍼를 재사용할 수 있게 된다.

 

네티의 바이트 버퍼를 바이트 버퍼 풀에 할당하려면 ByteBufAllocator 인터페이스를 사용하면 된다.

즉 ByteBufAllocator 의 하위 추상 구현체인 PooledByteBufAllocator 클래스로 각 바이트 버퍼를 생성한다.

public class PooledByteBufAllocator extends AbstractByteBufAllocator implements ByteBufAllocatorMetricProvider {...}

 

🔹네티 바이트 버퍼 생성 시 고려사항

  • 풀링 여부: 바이트 버퍼를 풀에서 관리할지 여부
  • 다이렉트 버퍼 여부: 바이트 버퍼를 다이렉트 버퍼로 생성할지 여부

네티 바이트 버퍼 종류

  풀링 O 풀링 X
힙 버퍼 PooledHeapByteBuf UnpooledHeapByteBuf
다이렉트 버퍼 PooledDirectByteBuf UnpooledDirectByteBuf

네티 바이트 버퍼 생성 방법

  풀링 O 풀링 X
힙 버퍼 ByteBufAllocator.DEFAULT.heapBuffer() Unpooled.buffer()
다이렉트 버퍼 ByteBufAllocator.DEFAULT.directBuffer() Unpooled.directBuffer()

⇒ 인수를 지정하지 않고 생성하면 네티에 지정된 기본값인 256바이트 크기의 바이트 버퍼가 생성된다.

 

🔸버퍼 사용

바이트 버퍼 읽기 쓰기

읽기 인덱스와 쓰기 인덱스가 따로 존재하기 때문에 flip 메서드를 호출할 필요가 없다.

 

가변 크기 버퍼

자바는 한번 생성된 바이트 버퍼의 크기를 변경할 수 없었으나, 네티는 동적으로 변경가능하다.

buf.capacity(7);
..
buf.capacity(13);

위 코드와 같이 재정의해도 기록된 데이터는 남아있다.

 

바이트 버퍼 풀링

자바는 기본적으로 바이트 버퍼 풀을 제공하지 않기 때문에, 개발자가 직접 풀링을 구현하거나 라이브러리를 사용해야 했다.

 

하지만 네티는 바이트 버퍼 풀링을 자동으로 수행하여, 빈번한 버퍼 할당과 해제로 인한 가비지 컬렉션 횟수를 줄일 수 있다. 이를 통해 JVM의 가비지 컬렉션 성능이 향상된다.

 

+) 가비지 컬렉션: JVM이 메모리가 부족할 때 불필요한 객체를 정리하는 작업이다. 메모리 크기나 객체 수가 많아질수록 가비지 컬렉션에 걸리는 시간이 늘어난다.

 

[바이트 버퍼 풀링 메커니즘]

ByteBufAllocator를 사용해 생성된 바이트 버퍼는 풀에 저장된다. 또한, ReferenceCountUtil 클래스의 retain과 release 메서드를 사용하여 참조수를 관리한다.

  • retain: 참조수를 증가시킨다.
  • release: 참조수를 1 감소시키며 메모리를 해제한다.

 

부호없는 값 읽기

자바에서는 1바이트 데이터를 부호 없는 데이터로 변환하려면 2바이트 데이터형에 데이터를 저장해야 한다. 네티는 부호 없는 값을 읽을 수 있는 getUnsignedXXX 메서드를 제공한다.

 

엔디안 변환

네티의 바이트 버퍼는 기본적으로 빅엔디안 형식을 사용한다. order() 메서드를 사용하여 바이트 버퍼의 엔디안을 변환할 수 있으며, 이 메서드는 새로운 버퍼를 생성하지 않고 기존 버퍼의 내용을 공유하는 파생 객체를 만든다.

 

바이트 버퍼 상호 변환

네티 바이트 버퍼는 nioBuffer() 메서드를 통해 자바 NIO 버퍼로 변환할 수 있다. 변환된 NIO 바이트 버퍼는 네티 바이트 버퍼의 내부 배열을 공유하며, 두 버퍼는 내용을 공유하게 된다.

 

+) 내부를 고유하는 바이트 버퍼를 뷰 버퍼라고 한다.

 

채널과 바이트 버퍼 풀

네티는 channelRead() 메서드를 사용해 데이터를 처리할 때 네티 바이트 버퍼를 사용한다. 이 바이트 버퍼는 채널 읽기 후 바이트 버퍼 풀로 반환된다. 네티 바이트 버퍼 풀은 서버 소켓 채널이 초기화될 때 함께 초기화되며, ChannelHandlerContext 인터페이스의 alloc() 메서드를 통해 참조할 수 있다.