13. 버퍼 06. 버퍼 만들기

06. 버퍼 만들기

버퍼는 new 생성자를 이용하지 않고 allocate() 팩토리 메소드를 사용했다.

public abstract class ByteBuffer extends Buffer implements Comparable {
    // 팩토리 메소드를 사용해서 만드는 방법
    public ByteBuffer allocate(int capacity);

    // 이미 존재하는 배열을 이용해서 만드는 방법
    public static ByteBuffer wrap(byte[] array);
    public static ByteBuffer wrap(byte[] array, int offset, int length);

    // 유틸리티 메소드들
    public final boolean hasArray();
    public final byte[] array();
    public final int arrayOffset();
}

버퍼의 생성과 관련된 API.

여기에서 보이는 것들은 ByteBuffer에서 제공되는 메소드들이지만 다른 버퍼들도 파라미터와 리턴 값만 다르고 사용법은 동일하다. 우선 ByteBuffer 클래스가 추상 클래스이다. 부모 클래스인 Buffer와 일곱가지 각각의 Buffer 클래스의 하위 클래스들은 모두 추상클래스다. 따라서 생성자를 통해 직접적으로 객체를 만들 수 없고 대신 API에서 볼 수 있듯이 팩토리 메소드를 이용해서 만드는 방법과 존재하는 배열을 이용해서 만드는 방법을 제공한다.

ByteBuffer buf = ByteBuffer.allocate(1000);

이렇게 생성된 buf는 각 기본 속성 값들이 다음과 같이 초기화 된다. mark = -1; position = 0; limit = 1000; capacity = 1000;

TIP. 팩토리 메소드를 사용하는 이유는 생성자의 한계를 극복하기 위해서이다. 자바의 생성자를 통해 어떤 객체를 생성하면 항상 해당 형식의 인스턴스만을 생성해서 리턴할 뿐, 그 객체의 하위 클래스 형식을 리턴하지 못한다. 이것은 우리가 생성자 안에서 ㅇㅓ떤 구현을 통해 객체를 리턴할 기회조차도 갖지 못한 상태로 언어적 차원에서 자동적으로 이루어진다. 따라서 어떤 객체의 하위 형식을 리턴하려면 생성자와 같은 동작을 하는 정적 메소드, 즉 팩토리 메소드를 사용해야 한다.

NIO에서 ByteBuffer의 allocate() 메소드 호출로 생성되는 실제 형식은 ByteBufferㅏ 아닌 이것의 하위 형식인 HeapByteBuffer다. 다음은 allocate() 메소드의 실제 구현부분인데, HeapByteBuffer가 리턴되는 것을 확인할 수 있다.

public static ByteBuffer allocate(int capacity) {
    if (capacity < 0)
        throw new IllegalArgumentException();
    return new HeapByteBuffer(capacity, capacity);
}

이렇게 팩토리 메소드를 사용함으로써 프로그래머는 인터페이스에 해당하는 ByteBuffer만을 바라보며 사용할 수 있고 API 개발자는 실제 구현에 해당하는 부분을 HeapByteBuffer에서 캡슐화할 수 있게 된다.

Wrap() 메소드

byte[] myArray = new byte[1024];
ByteBuffer buf = ByteBuffer.wrap(myArray);

이 코드로 버퍼를 생성하면 1024의 크기를 갖는 버퍼가 생성된다. 이때는 buf는 myArray를 참조하기 떄문에 myArray나 buf 중 하나를 변경하면 양쪽 모두에게 반영된다.

Wrap() 메소드는 배열의 특정 범위 만큼만 사용하도록 버퍼를 생성하는 방법도 제공한다.

byte[] myArray = new byte[1024];
ByteBuffer buf = ByteBuffer.wrap(myArray, 10, 100);

이 코드로 생성된 버퍼는 position이 10, limit가 100으로 설정되어 생성된다. 하지만 slice()로 생성되는 버퍼처럼 일부분을 잘라내서 새로 생성하는 것이 아닌, myArray 전체를 그대로 생성하되 파라미터로 설정된 부분만을 이용할 수 있도록 초기화되어 생성된다. 따라서 clear() 메소드를 호출하면 myArray의 0부터 1024까지 접근해서 읽거나 쓸 수 있게 된다.

이 메소드는 주로 배열에 저장된 데이터를 채널로 보내거나 채널의 데이터를 배열로 읽어들이고 싶을떄 유용하게 사용된다.

allocate(), wrap() 메소드를 이용해서 생성된 버퍼는 모두 시스템 메모리가 아닌 JVM의 힙 영역에 저장되는 Non-direct 버퍼다. 넌다이렉트 버퍼에는 모두 내부적으로 해당 버퍼 형식의 보조 배열이 있다. allocate() 메소드로 생성한 버퍼도 앞서 wrap() 메소드로 생성한 버퍼도 앞서 wrap() 메소드로 생성한 버퍼가 파라미터로 넘겨준 배열을 실제 저장소로 사용하듯이 내부적으로 그렇게 보조 배열을 만들어서 저장소로 사용하는 것이다.

이것은 hasArray() 메소드 호출로 알 수 있다. allocate(), wrap() 메소드로 생성한 버퍼에 hasArray() 메소드를 호출하면 true가 리턴되는 것을 확인할 수 있다. 또한 array() 메소드ㄹㅗ 실제 저장소에 해당하는 보조 배열을 넘겨받을 수도 있고 arrayOffset() 메소드로 보조 배열 내에 있는 이 버퍼의 최초 요소의 오프셋을 확인할 수도 있다.

반면 시스템 메모리를 사용하는 다이렉트 버퍼의 경우 hasArray() 메소드를 호출하면 UnsupportedOperationException이 발생한다. 따라서 array(), arrayOffset() 메소드도 사용할 수 없고 이들은 호출하면 역시 UnsupportedOperationException이 발생한다.

버퍼 클래스들은 필요에 따라 프로세스 메모리를 사용할지, 시스템 메모리를 사용할지를 결정할 수 있도록 디자인되어 잇다. 하지만 시스템 메모리를 사용할 수 있는 다이렉트 버퍼는 ByteBuffer만 생성할 수 있다.

그 이유로 버퍼에는 메모리 맵 파일에 사용하는 ByteBuffer의 하위 클래스인 MappedByteBuffer가 있다. 이버퍼는 파일채녈(FileChannel)과 함께 메모리 맵 파일을 구현하는 데 사용되는 다이렉트 버퍼인데 파일 채널을 통해 생성해야 한다.

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