바이트 버퍼
네티의 바이트 버퍼
네티에서는 자바의 ByteBuffer를 그대로 사용하지 않고 자체적으로 바이트 버퍼를 구현해 사용한다.
빈번한 바이트 버퍼 할당 및 해제에 대한 부담을 줄여주어 GC에 대한 부담을 줄여준다.
다음은 자바 ByteBuffer와의 차이점이다.
별도 읽기 인덱스, 쓰기 인덱스 보유
flip() 메서드 없이 읽기 쓰기 작업 전환
용량을 필요에 따라 확장 가능
바이트 버퍼 풀
내장 복합 버퍼 형식으로 zero copy 달성 가능
자바 바이트 버퍼와 네티 바이트 버퍼 상호 변환
읽기, 쓰기에 대해 인덱스가 각각 존재하기 때문에 별도의 flip 메서드 호출 없이 읽기와 쓰기가 가능하다.
하나의 바이트 버퍼에 읽기 작업과 쓰기 작업을 동시에 수행할 수도 있다.
바이트 버퍼 종류
힙 버퍼
보조 배열(backing array)이라고 불리며 가장 자주 이용되는 방식이다.
JVM의 힙 공간에 데이터를 저장하여, 풀링이 사용되지 않는 경우 할당과 해제 속도가 빠르다.
다이렉트 버퍼
JDK의 ByteBuffer 클래스는 네이티브 호출을 통해 운영체제의 커널 영역에 메모리를 할당하도록 허용하여 네이티브 입출력 작업을 호출하기 전후에 버퍼의 내용을 중간 버퍼로 복사하지 않게 할 수 있다.
힙 버퍼를 사용하면 소켓 통신 전에 JVM 힙 공간에 저장되어 있는 버퍼를 다이렉트 버퍼로 복사해야 한다. 반면 다이렉트 버퍼는 가비지 컬렉션이 적용되지 않는 힙 바깥에 저장되어 소켓 통신에 그대로 사용할 수 있다.
버퍼 할당과 해제에 대한 비용이 힙 버퍼 방식에 비해 크다.
컨테이너의 데이터를 배열로 접근해야하는 경우 아래와 같이 복사해주는 로직이 필요하므로 힙 버퍼 방식이 편할 수 있다.
복합 버퍼
여러 ByteBuf의 집합을 사용하는 패턴으로, ByteBuf 인스턴스를 필요에 따라 추가/삭제할 수 있다.
네티는 CompositeByteBuf 클래스를 제공하여 복합 버퍼를 사용할 수 있도록 한다.
헤더와 본문으로 구성되는 HTTP 메시지를 복합 버퍼로 묶어 사용할 수 있다.
CompositeByteBuf를 사용하는 소켓 입출력 작업을 최적화해 JDK ByteBuffer에 비해 성능과 메모리 소비 면에서 이점이 있다.
바이트 버퍼 사용 방법
ByteBuf 클래스의 사용 방법을 알아본다.
버퍼 읽고 쓰기
버퍼에 쓸 때는
writeXXX()
메서드를 사용하면 된다. writeInt, writeLong, writeChar, writeBytes 등 다양한 메서드를 지원한다.버퍼에서 읽을 때는
readXXX()
메서드를 사용하면 된다. 마찬가지로 readInt, readLong, readBytes 등 다양한 메서드를 지원한다.아래 코드에서는 4 bytes 짜리 정수를 바이트 버퍼에 입력하고, 2 bytes만 바이트 버퍼로부터 읽는다.
65537은 16진수로 나타내면 0x10001이고, 이 값에 4 bytes 패딩을 하면 0x00010001이 된다. 따라서 앞쪽 2 bytes를 읽으면 1이 된다.
아래는 읽을 수 있는 바이트를 모두 읽는 코드이다.
getXXX() 메서드와 setXXX() 메서드를 이용해서도 읽기와 쓰기를 할 수 있으나 이 과정에서 인덱스를 변경시키지 않는다.
아래와 같이 특정 바이트를 수정, 조회하더라도 readerIndex, writerIndex에 변화가 없는 것을 확인할 수 있다.
인덱스 관리
markReaderIndex/markWriterIndex
메서드와resetReaderIndex/resetWriterIndex
메서드를 제공하여 인덱스를 지정하거나 초기화할 수 있다.readerIndex(int)
,writerIndex(int)
메서드를 통해 지정한 위치로 인덱스를 이동할 수도 있다.
특정 바이트 검색
ByteBufProcessor의 편의성 메서드를 이용해 특정 문자를 검색할 수 있다.
다음은 '\r' 문자의 인덱스를 바이트 버퍼에서 찾는 코드이다.
버퍼 초기화
clear()
메서드를 통해 버퍼 데이터를 초기화할 수 있다.
혹은
discardReadBytes()
메서드를 이용해 이미 읽은 부분은 배열에서 제거하고 앞으로 읽을 부분과 기록 가능한 남은 부분만을 배열에서 남길 수 있다.하지만 배열의 앞부분을 제거하고 뒷 부분을 앞으로 땡겨오기 위해 메모리 복사가 이뤄지므로 메모리가 아주 중요한 상황 등 꼭 필요할 경우에만 사용해야 한다.
파생 버퍼
바이트 버퍼의 일부 혹은 전체를 이용해 새로운 바이트 버퍼 인스턴스를 만들어낼 수 있으며, 내부의 데이터는 공유된 형태이므로 내용을 수정하면 두 인스턴스 수정된 값을 갖게 된다.
duplicate()
,slice()
,slice(int, int)
,Unpooled.unmodifiableBuffer(...)
,order(ByteOrder)
,readSlice(int)
메서드를 사용해 파생 버퍼를 만들 수 있다.다음은 slice 메서드를 이용해 buf 객체의 일부만 잘라내어 새로운 파생 버퍼를 만드는 코드이다.
단순히 바이트 버퍼의 복제본이 필요하다면
copy()
,copy(int, int)
메서드를 사용할 수 있으며, 이 경우 내부의 데이터가 공유되지 않는 독립된 객체가 생성된다.
가변 크기 버퍼
자바의 바이트 버퍼와 달리 네티 바이트 버퍼는 크기가 가변적이다.
capacity 메서드에 원하는 크기를 입력해 바이트 버퍼의 크기를 변경할 수 있다.
기존 존재하는 데이터보다 크기를 작게 설정하면 데이터가 잘리게 된다.
부호 없는 값 읽기
자바의 모든 원시 데이터형은 모두 부호 있는 데이터로 저장되기 때문에, C 언어로 작성된 애플리케이션과 네트워크 통신 시 부호 없는 데이터를 처리하기 곤란하다.
네티에서는 getUnsignedXXX() 메서드를 사용해 부호 없는 값을 읽을 수 있도록 한다.
엔디안 변환
네티 바이트 버퍼의 기본 엔디안은 자바와 동일하게 빅 엔디안 이다.
리틀 엔디안이 필요한 상황에서 order 메서드를 사용해 엔디안을 변환할 수 있다.
단, 새로운 바이트 버퍼를 생성하는 것이 아닌 주어진 바이트 버퍼 내용을 공유하는 파생 바이트 버퍼 객체를 만드므로 주의해야 한다.
자바 바이트 버퍼 변환
nioBuffer 메서드를 사용해 자바 바이트 버퍼로 변환할 수 있다.
네티 바이트 버퍼의 내부 바이트 배열을 공유하므로 주의해야 한다.
자바 바이트 버퍼를 네티의 바이트 버퍼로 변환할 수도 있다.
ByteBufHolder
실제 데이터 페이로드와 함께 다양한 속성 값을 저장해야 하는 경우 ByteBufHolder를 사용해 페이로드를 저장하는 메시지 객체 구현 시 용이하도록 한다.
copy(), duplicate() 메서드를 제공하여 ByteBuf의 복제본을 얻을 수 있도록 한다.
바이트 버퍼 풀
네티는 프레임워크 레벨에서 바이트 버퍼 풀을 제공하여 바이트 버퍼를 재사용할 수 있도록 한다.
버퍼를 빈번히 할당하고 해제할 때 일어나는 오버헤드와 GC 횟수를 감소시킬 수 있다.
버퍼 풀을 사용하는지 아닌지, 그리고 데이터를 힙 메모리에 저장하는지 운영체제의 커널 영역에 저장하는지에 따라 각각 다른 클래스를 제공한다.
바이트 버퍼 객체를 생성하는 방법은 아래와 같다.
ByteBufAllocator의 참조는 Channel이나 ChannelHandlerContext를 통해 얻을 수 있다.
ByteBufAllocator에서 제공하는 메서드는 아래와 같다.
힙 버퍼 혹은 다이렉트 버퍼를 얻을 수 있는 buffer 메서드
힙 버퍼를 얻을 수 있는 heapBuffer 메서드
다이렉트 버퍼를 얻을 수 있는 directBuffer 메서드
복합 버퍼를 얻을 수 있는 compositeBuffer, compositeHeapBuffer, compositeDirectBuffer 메서드
소켓 입출력 작업에 사용될 바이트 버퍼를 얻을 수 있는 ioBuffer 메서드
네티의 채널에서 사용되는 바이트 버퍼 역시 바이트 버퍼 풀을 이용하며, 이벤트 메서드가 종료되면 바이트 버퍼 객체는 풀로 돌아가게 된다.
ByteBufUtil
ByteBuf를 조작하기 위한 정적 도우미 메서드들을 제공하는 클래스이다.
ByteBuf 내용을 16진수로 출력하는 hexdump() 메서드는 디버깅을 위한 로깅에 사용될 수 있으며, equals() 메서드도 제공하여 두 ByteBuf 객체가 같은지 비교할 수 있다.
참조 카운팅
다른 객체에서 더이상 참조하지 않는 객체가 보유한 리소스를 해제해 메모리 사용량과 성능을 최적화하는 기법
특정 객체에 대한 활성 참조의 수를 추적하며 참조 카운트가 0이 되면 객체가 할당 해제되어 더이상 사용할 수 없게 만든다.
풀링 구현에서 메모리 할당의 오버헤드를 줄이는 데에 반드시 필요하다. 바이트 버퍼의 참조 수를 관리하기 위해 ReferenceCountUtil 클래스의 retain, release 메서드를 사용한다.
Last updated