본문 바로가기

Java/Vert.x

버텍스 코어: 버퍼(Buffers)

이 문서의 내용

    더보기

    버퍼(Buffers)는 0 또는 그 이상으로 표현되는 바이트(Bytes)의 연속입니다.

    버텍스의 버퍼는 바이트를 Write하는 과정에서 자동으로 크기가 확장되는 일종의 스마트 바이트 배열입니다.

    버퍼 초기화

    버퍼는 정적 메소드 Buffer.buffer()를 사용하여 생성합니다. 빈 버퍼를 생성하려면 다음과 같이 작성합니다.

    Buffer buff = Buffer.buffer();

    문자열을 사용하여 초기화도 가능합니다. 문자열은 UTF-8로 인코딩됩니다.

    Buffer buff = Buffer.buffer("some string");

    문자열의 인코딩 유형을 지정하려면 다음과 같이 작성합니다.

    Buffer buff = Buffer.buffer("some string", "UTF-16");

    바이트 배열(byte[])을 사용하는 경우 다음과 같이 작성합니다.

    byte[] bytes = new byte[] {1, 3, 5};
    Buffer buff = Buffer.buffer(bytes);

    버퍼가 사용할 메모리 사이즈를 예측 할 수 있다면 필요한 메모리를 사전에 할당 할 수 있습니다.

    사전에 예측한대로 할당된 메모리 내에서 버퍼가 Write 된다면 자동 확장으로 인한 과정보다 더 효율적입니다.

    Buffer buff = Buffer.buffer(10000);

    버퍼 읽기/쓰기

    버퍼에 데이터를 작성하는 방법은 AppendingRandom access 두 가지가 있습니다.

    Appending은 작성하는 데이터 유형에 따라서 appendXXX 메소드를 사용합니다. 이 방법으로 Write되는 데이터는 버퍼의 뒤에 붙여집니다.

    Buffer buff = Buffer.buffer();
    buff.appendInt(123);
    buff.appendString("hello\n");

    Random access는 작성하는 데이터 유형에 따라서 setXXX 메소드를 사용합니다. 메소드의 첫 번째 인자는 접근하려는 버퍼의 인덱스입니다.

    이 방법으로 Write되는 데이터는 지정된 버퍼의 인덱스를 시작으로 이전 데이터를 덮어씌웁니다.

    Buffer buff = Buffer.buffer();
    buff.setInt(1000, 123);
    buff.setString(0, "hello");
    더보기

    어느 경우든 버퍼는 항상 바이트를 Write 할 수 있도록 자동으로 확장되므로 쓰기 작업 중에는 IndexOutOfBoundsException을 발생시키지 않습니다.

    다음 예시는 빈 버퍼를 생성하고 10000 인덱스에 엑세스 후 Int형 데이터 1을 작성합니다.

    버퍼에 엑세스하며 10000 바이트를 할당하고, Int형 데이터를 추가하기 위해 4 바이트를 추가로 확보하므로 버퍼의 크기는 10004 바이트가 됩니다.

    Buffer b = Buffer.buffer();
    b.setInt(10000, 1);
    System.out.println(b.length());

    버퍼의 데이터를 읽으려면 데이터 유형에 따라서 getXXX 메소드를 사용합니다. 메소드의 첫 번째 인자는 접근하려는 버퍼의 인덱스입니다.

    Buffer buff = Buffer.buffer();
    for (int i = 0; i < buff.length(); i += 4) 
    {
    	System.out.println("int value at " + i + " is " + buff.getInt(i));
    }
    코드 비고
    Line 2:4 for (int i = 0; i < buff.length(); i+= 4) { ... } 버퍼 전체에 걸쳐서 Int 데이터를 얻기 위해 4바이트씩 건너뛰며 엑세스합니다.

    부호가 없는 정수

    부호가 없는 정수는 getUnsignedXXX appendUnsignedXXXsetUnsignedXXX 메소드를 사용합니다.

    대게 대역폭 소모를 최소화 하기 위한 네트워크 프로토콜 구현 코덱(Codecs)에 사용됩니다. 다음 예시는 정수 값 200을 1바이트만으로 저장하고 읽습니다.

    Buffer buff = Buffer.buffer(128);
    int pos = 15;
    buff.setUnsignedByte(pos, (short) 200);
    System.out.println(buff.getUnsignedByte(pos));

    length(), copy(), slice()

    버퍼에서 많이 사용되는 함수 중 하나인 length()는 버퍼에 할당된 바이트 배열의 길이를 리턴합니다.

    Buffer b = Buffer.buffer(1000);
    // 1000
    System.out.println(b.length());

    copy()를 사용하면 원본 버퍼의 전체 바이트 배열을 다른 버퍼에 복사 할 수 있습니다.

    이때 데이터가 복사되는 방식은 값에 의한 복사 | 얕은 복사(Shallow copy)입니다.

    Buffer buffer = Buffer.buffer();
    buffer.appendInt(100);
    
    // 버퍼를 copy() 메소드로 복사
    Buffer copied = buffer.copy();
    // buffer: 100 & copied: 100
    System.out.println(buffer.getInt(0));
    System.out.println(copied.getInt(0));
    
    // 원래 버퍼의 값 수정
    buffer.setInt(0, 200);
    // buffer: 200 & copied: 100
    System.out.println(buffer.getInt(0));
    System.out.println(copied.getInt(0));
    
    // 복제 버퍼의 값 수정
    copied.setInt(0, 300);
    // buffer: 200 & copied: 300
    System.out.println(buffer.getInt(0));
    System.out.println(copied.getInt(0));

    slice()는 원본 버퍼의 특정 바이트 배열을 다른 버퍼에 복사합니다.

    이때 데이터가 복사되는 방식은 참조에 의한 복사 | 깊은 복사(Deep copy)입니다.

    Buffer buffer = Buffer.buffer();
    buffer.appendInt(100);
    		
    // 버퍼를 slice() 메소드로 복사
    Buffer sliced = buffer.slice();
    // buffer: 100 & sliced: 100
    System.out.println(buffer.getInt(0));
    System.out.println(sliced.getInt(0));
    		
    // 원래 버퍼의 값 수정
    buffer.setInt(0, 200);
    // buffer: 200 & sliced: 200
    System.out.println(buffer.getInt(0));
    System.out.println(sliced.getInt(0));
    		
    // 복제 버퍼의 값 수정
    sliced.setInt(0, 300);
    // buffer: 300 & sliced: 300
    System.out.println(buffer.getInt(0));
    System.out.println(sliced.getInt(0));

    버퍼 재사용

    버퍼는 소켓 등으로 Write한 이후 버퍼를 재사용 할 수 없습니다.

    버퍼를 소켓으로 Write하는 과정:send() 등은 비동기로 처리되므로 버퍼가 스트리밍 되는 과정에서 재사용으로 인해 값이 변경 될 수 있습니다.

    정리 및 복습

    • 버퍼는 바이트의 연속이며 Write 과정에서 자동으로 크기가 확장 될 수 있습니다.
    • 이로 인해 Write 과정에서 IndexOutOfBoundsException 오류가 발생하지 않는 특징이 있습니다.
    • 부호가 없는(unsigned) 데이터 유형을 사용하면 스트리밍을 위한 버퍼의 크기를 최적화 할 수 있습니다.
    • 버퍼를 복사하기 위한 copy(): 값에 의한 복사 | 얕은 복사(Shallow copy)이며 slice(): 참조에 의한 복사 | 깊은 복사(Deep copy)입니다.
    • 버퍼는 스트리밍 과정에서 값이 변경되면 위험하므로 재사용을 지양합니다.