13. 버퍼 07. ByteBuffer

07. ByteBuffer

7가지 버퍼 중 ByteBuffer와 CharBuffer는 다른 버퍼와 차별화되는 몇몇 특징이 있다. 그 중에서도 ByteBuffer는 시스템 메모리를 직접 사용하는 다이렉트 버퍼를 만들 수 있는 유일한 버퍼 클래스기 때문에 가장 중요하다.

1. 다이렉트 버퍼

NIO의 많은 버퍼 중 왜 ByteBuffer만 다이렉트 버퍼로 만들 수 있게 했을까? 운영체제가 이용하는 가장 기본적인 데이터 단위가 바이트이고 시스템 메모리 또한 순차적인 바이트들의 집합이기 때문이다.

Int, Long, Float, Double, Char, Object 등 자바 프로그램에서 많은 데이터 형식들을 사용하지만 JVM은 운영체제와 IO 수행을 위해 통신할 때 데이터 모두를 운영체제가 인식할 수 있는 순차적인 바이트 형태로 바꿔서 전달한다. 또한 시스템 메모리의 구성 형태와 달리 자바에서 사용하는 바이트 배열은 시스템 메모리에서 사용하는 순차적인 바이트들의 집합이 아닌 객체 내에 바이트들을 저장하고 있는 형태기 때문에 커널이 관리하는 시스템 메모리에 직접 저장할 수 있는 형태가 아니다.

이러한 한계점을 극복하기 위해 NIO에서 다이렉트 버퍼가 도입되었고 ByteBuffer만이 시스템 메모리를 사용할 수 있는 다이렉트 버퍼로 생성할 수 있는 것이다

API 코드

public abstract class ByteBuffer extends Buffer implements Comparable {
    public static ByteBuffer allocateDirect(int capacity);
    public abstract boolean isDirect();
}

템플릿 코드

ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
boolean isDirect = directBuffer.isDirect();

allocate() 메소들 이용한 것과 메소드 이름만 다를 뿐, 사용법은 같다는 것을 쉽게 알 수 있다. allocateDirect() 메소드를 호출하면 다이렉트 버퍼가 생성된다. 넌다이렉트 버퍼와 달리 다이렉트 버퍼를 생성하기 위한 메소드는 allocateDirect() 메소드 하나뿐이다. 또한 해당 버퍼가 다이렉트 버퍼인지 아닌지를 확인하기 위해서 isDirect() 메소드를 제공한다. 리턴 값이 true면 다이렉트 버퍼이고, false이면 넌다이렉트 버퍼이다.

이렇게 생성된 다이렉트 버퍼는 채널과 네이티브 IO 루틴들이 이용하게 된다. 메모리 영역 안에서 채널을 이용하여 바이트들을 직접적으로 다이렉트 버퍼에 저장할 수 있고 또한 네이티브 코드를 통해 운영체제가 직접적으로 메모리 영역에서 다이렉트 버퍼에 데이터를 읽고 쓸 수 있게 된다. 따라서 효율적인 IO 작업이 가능하다.

자바는 C/C++와 같은 저수준 언어가 아닌데 어떻게 시스템 메모리인 다이렉트 버퍼를 생성하고, 조작하고, 가비지 컬렉트까지 할 수 있을까? 답은 JNI이다.

다시 본론으로 와서 ByteBuffer.allocateDirect() 메소드를 이용해서 다이렉트 버퍼를 생성하면 내부적으로 다음과 같은 일들이 벌어진다.

  • 메소드 파라미터로 주어진 크기만큼의 시스템 메모리를 JNI를 이용해 할당한다.
  • 이 시스템 메모리를 래핑하는 자바 객체를 만들어 리턴한다.

우리가 생성해서 리턴 받은 객체는 시스템 메모리를 래핑하고 있는 자바 객체이므로 우리는 이 객체를 사용해서 직접적인 시스템 메모리를 제어할 수 있고 곧바로 시스템 메모리에 반영될 것이다. 이 객체는 가비지 컬랙트 되면 이 버퍼가 래핑하고 있는 주소의 시스템 메모리도 동시에 안전하고 확실하게 해제(deallocate 또는 release) 된다.

채널의 타겟이 되는 것은 다이렉트 버퍼 뿐이다. 넌다이렉트 버퍼를 채널의 탁[ㅅ으로 설정하면 다음과 같은 과정을 거치기 때문에 성능이 저하된다.

  1. 넌다이렉트 버퍼를 채널에 전달한다.
  2. 임시로 사용할 다이렉트 버퍼를 생성한다.
  3. 넌다이렉트 버퍼에서 임시로 만든 다이렉트 버퍼로 데이터를 복사한다.
  4. 임시 버퍼를 사용해서 채널이 저수준 IO를 수행한다.
  5. 임시 버퍼가 다 사용되면 마지막으로 임시 버퍼를 가비지 컬렉트한다.

다이렉트 버퍼를 메모리에 로드하거나 릴ㄹ리즈 하는것은 넌다이렉트 버퍼를 생성, 제거하는 것에 비해 상당히 부하가 크므로 다이렉트 버퍼 풀을 만들어서 사용하는 것이 좋은 선택이 될 수 있다.

2. 뷰 버퍼

ByteBuffer에서는 특별한 형태의 뷰 버퍼를 만들 수 있다. 이와 비슷한 형태인 duplicate()와 slice() 메소드로 생성되는 버퍼가 있다. ByteBuffer에서도 position, mark, limit, capacity를 갖고 원본 버퍼와 데이터를 공유하는, 자신을 제외한 다른 여섯 가지 기본 형식의 뷰 버퍼를 생성할수 있는 팩토리 메소드를 제공한다.

public abstract class ByteBuffer extends Buffer implements Comparable {
    public abstract CharBuffer asCharBuffer();
    public abstract ShortBuffer asShortBuffer();
    public abstract asIntBuffer();
    public abstract asLongBuffer();
    public abstract asFloatBuffer();
    public abstract asDoubleBuffer();
}

이 API에서 볼 수 있듯 다른 기본 형식 버퍼들의 뷰 버퍼를 리턴하는 메소드 여섯개가 있다.

import java.nio.ByteBuffer;
import java.nio.IntBuffer;

public class ViewBufferTest {
    public static void main(String[[] args) {
        //크기가 10인 ByteBuffer를 생성한다.
        ByteBuffer buf = ByteBuffer.allocate(10);

        //뷰 버퍼인 IntBuffer를 생성한다.
        IntBuffer ib = buf.asIntBuffer();
        //뷰 버퍼의 초기 값을 출력한다.
        System.out.println("position = " + ib.position() + ", limit = " + ib.limit() + ", capacity = " + ib.capacity());
        //뷰 버퍼에 데이터를 쓴다.
        ib.put(1024).put(2048);
        //뷰 버퍼의 데이터를 출력한다.
        System.out.println("index_0 = " + ib.get(0) + ", index_1 = " + ib.get(1));

        //원본 버퍼도 변경되었는지 확인하기 위해 출력한다.
        while(buf.hasRemaining()) {
            System.out.print(buf.get() + " ");
        }
    }
}

3. 다른 데이터 형식으로 읽고 쓰기

4. 바이트 순서

출처 : 김성박 송지훈 공저, 『 자바 IO & NIO 네트워크 프로그래밍』, 한빛미디어(2004.9.30), 13장 인용