자바

자바 NIO 버퍼 - Buffer 의 종류, Buffer 생성 버퍼 생성, 버퍼 위치 속성

알통몬_ 2017. 4. 16. 18:21
반응형


안녕하세요 알통몬입니다.

공감 및 댓글은 포스팅 하는데 아주아주 큰 힘이 됩니다!!

포스팅 내용이 찾아주신 분들께 도움이 되길 바라며

더 깔끔하고 좋은 포스팅을 만들어 나가겠습니다^^

 


버퍼란 읽고 쓰기가 가능한 메모리의 배열을 말합니다.

NIO에서는 데이터의 입출력을 위해서 항상 버퍼를 사용해야 합니다.

버퍼를 이해하고 제대로 잘 사용할 수 있어야 NIO에서 제공하는 API를 제대로 사용할 수 있습니다.


버퍼의 종류

- 데이터 타입에 따른 버퍼

 : NIO 버퍼는 저장되는 데이터 타입에 따라 Buffer 추상 클래스를 상속하는 별도의 클래스를 제공합니다.


MappedByteBuffer 는 ByteBuffer의 하위 클래스로써 파일의 내용에 랜덤하게 접근하기 위해서

파일의 내용을 메모리에 매핑시킨 버퍼입니다.



- 넌다이렉트 버퍼와 다이렉트 버퍼

 : 버퍼가 사용하는 메모리의 위치에 따라서 넌다이렉트 버퍼와 다이렉트 버퍼로 구분됩니다.

넌다이렉트 버퍼란 JVM이 관리하는 힙 메모리 공간을 이용하는 버퍼입니다.

다이렉트 버퍼란 운영체제가 관리하는 메모리 공간을 이용하는 버퍼입니다.

넌다이렉트 와 다이렉트 크기 비교 예제)

import java.nio.ByteBuffer;


public class Example {

 public static void main(String[] args) {

  ByteBuffer directBuffer = ByteBuffer.allocateDirect(200 * 1024 * 1024); 

  System.out.println("다이렉트 버퍼가 생성.");

  

  ByteBuffer nonDirectBuffer = ByteBuffer.allocate(200 * 1024 * 1024);

  System.out.println("넌다이렉트 버퍼가 생성.");

 }

}

시스템 성능에 따라 버퍼의 크기가 유동적이므로 적절히 크기를 조정하여 테스트 해보면 다이렉트 버퍼의 크기가 더 큼.


예제2) 버퍼 성능 비교

package sec03.exam01_direct_buffer;


import java.nio.ByteBuffer;

import java.nio.channels.FileChannel;

import java.nio.file.Files;

import java.nio.file.Path;

import java.nio.file.Paths;

import java.nio.file.StandardOpenOption;

import java.util.EnumSet;


public class Example2 {

 public static void main(String[] args) throws Exception {

  Path from = Paths.get("디렉토리경로/house.jpg");

  Path to1 = Paths.get("디렉토리경로/house2.jpg");

  Path to2 = Paths.get("디렉토리경로/house3.jpg");

  

  long size = Files.size(from);

  

     FileChannel fileChannel_from = FileChannel.open(from);

     FileChannel fileChannel_to1 = FileChannel.open(to1, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));

     FileChannel fileChannel_to2 = FileChannel.open(to2, EnumSet.of(StandardOpenOption.CREATE, StandardOpenOption.WRITE));

     

     ByteBuffer nonDirectBuffer = ByteBuffer.allocate((int) size);

     ByteBuffer directBuffer = ByteBuffer.allocateDirect((int)size);

     

     long start, end;

     

     start = System.nanoTime();

     for(int i=0; i<100; i++) {

      fileChannel_from.read(nonDirectBuffer);

      nonDirectBuffer.flip();

      fileChannel_to1.write(nonDirectBuffer);

      nonDirectBuffer.clear();

     }

     end = System.nanoTime();

     System.out.println("넌다이렉트:\t" + (end-start) + " ns");

     

     fileChannel_from.position(0);

     

     start = System.nanoTime();     

     for(int i=0; i<100; i++) {

      fileChannel_from.read(directBuffer);

      directBuffer.flip();

      fileChannel_to2.write(directBuffer);

      directBuffer.clear();

     }

     end = System.nanoTime();

     System.out.println("다이렉트:\t" + (end-start) + " ns");

     

     fileChannel_from.close();

     fileChannel_to1.close();

     fileChannel_to2.close();

 }

 

다이렉트 버퍼는 채널을 사용하여 버퍼의 데이터를 읽고 저장할 경우에만 운영체제의 native IO를 수행합니다.

채널을 사용하지 않고 ByteBuffer의 get(), put()을 사용해 버퍼의 데이터를 읽고 저장한다면

내부적으로 JNI 를 호출해 native IO 를 수행하므로 JNI 호출이라는 오버헤더가 추가됩니다.

그렇기 때문에 오히려 넌다이렉트 버퍼의 get(), put()의 성능이 더 좋게 나올 수도 있습니다.

JNI : Java Native Interface 는 자바 코드에서 C 함수를 호출할 수 있도록 해주는 API 입니다.


버퍼 생성 :

데이터 타입 별로 넌다이렉트 버퍼를 생성하려면 각 버퍼 클래스의 allocate() 와 wrap() 을 호출하면 되고,

다이렉트 버퍼는 ByteBuffer 의 allocateDirect() 를 호출하면 됩니다.

ByteBuffer byteBuffer = ByteBuffer.allocate(100); => 최대 100개 바이트 저장 가능

CharBuffer charBuffer = CharBuffer.allocate(1000); => 최대 1000개 문자 저장 가능


warp() 메소드

 : 각 타입 별 버퍼 클래스는 모두 wrap() 을 가지고 있습니다

wrap() 은 이미 생성되어 있는 자바 배열을 래핑하여 버퍼 객체를 생성합니다.

자바 배열은 JVM 힙 메모리에서 생성되기 때문에 wrap()은 넌다이렉트 버퍼를 생성합니다.

예제)

byte[] byteArray = new byte[100];

ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);

char[] charArray = new char[100];

CharBuffer charBuffer = CharBuffer.wrap(charArray);


배열의 모든 데이터가 아니라 일부 데이터만 가지고 Buffer 객체 생성 가능.

이런 경우 인덱스의 길이를 추가적으로 지정.

ex)

byte[] byteArray = new byte[100];

ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray, 0, 50);

char[] charArray = new char[100];

CharBuffer charBuffer = CharBuffer.wrap(charArray, 0, 50);


CharBuffer는 CharSequence 타입의 매개값을 갖는 wrap() 메서드도 제공

String이 CharSequence 인터페이스를 구현했으므로 매개밧으로 문자열을 제공하여

CharBuffer을 생성할 수도 있음.

CharBuffer charBuffer = CharBuffer.wrap("NIO입출력은 버퍼 이용"); 


allocateDirect() 메소드

 : 운영 체제가 관리하는 메모리에 다이렉트 버퍼를 생성합니다.

이 메소드의 경우 각 타입 별 Buffer 클래스에는 없고, ByteBuffer에서만 제공합니다.

각 타입 별로 다이렉트 버퍼를 생성하기 위해서는 먼저 ByteBuffer 의 allocateDirect()로 버퍼를 생성한 후

ByteBuffer의 asCharBuffer(), asShortBuffer() 등의 메소드를 이용해 해당 타입 별로 Buffer를 얻으면 됩니다.

예)

ex)

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);  // 100개 byte 값 저장

CharBuffer charBuffer = ByteBuffer.allocateDirect(100).asCharBuffer(); // 50개의 char 값 저장

IntBuffer intBuffer = ByteBuffer.allocateDirect(100).asIntBuffer(); // 25개 int 값 저장


예제)

import java.nio.ByteBuffer;

import java.nio.CharBuffer;

import java.nio.IntBuffer;


public class DirectBufferCapacityExample {

 public static void main(String[] args) {

  ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100);

  System.out.println("저장용량: " + byteBuffer.capacity() + " 바이트");

  

  

  CharBuffer charBuffer = ByteBuffer.allocateDirect(100).asCharBuffer();

  System.out.println("저장용량: " + charBuffer.capacity() + " 문자");

  

  IntBuffer intBuffer = ByteBuffer.allocateDirect(100).asIntBuffer();

  System.out.println("저장용량: " + intBuffer.capacity() + " 정수");

 }

 

}

byteOrder -> 바이트 해석 순서

: 운영체제마다 데이터의 처리 순서 차이가 있습니다.

이런 차이는 데이터를 다른 운영체제로 보내거나 받을 경우 영향을 미치기 때문에

데이터를 다루는 버퍼도 이것을 고려해야 합니다.

앞쪽 바이트를 먼저 처리하는 것을 Big Endian, 뒤쪽 바이트를 먼저 처리하는 것을 Little Endian이라고 하며

Little Endian으로 동작하는 운영체제에서 만든 데이터 파일을 Big Endian으로 동작하는 운영체제에서

읽는 다면 ByteOrder 클래스로 데이터 순서를 맞춰야 합니다.

nativeOrder() 는 현 재 동작하고 있는 운영 체제가 Big Endian 인지 Little Endian 인지 알려줍니다.

JVM은 무조건 Big Endian으로 동작하도록 되어 있습니다.

예제)

import java.nio.ByteOrder;


public class ComputerByteOrderExample {

 public static void main(String[] args) {

  System.out.println("운영체제 종류 : "+System.getProperty("os.name"));

  System.out.println("네이티브의 바이트 해석 순서 :"+ByteOrder.nativeOrder());

 }

}


버퍼 위치 속성

 : 버퍼의 위치 속성의 개념과 위치 속성이 언제 변경되는지 보겠습니다.


위 속성의 크기를 비교하면

 0 <= mark <= positon <= limit <= capacity


예)

1. 7바이트 크기의 버퍼가 있다고 가정했을 때 처음에는 limit과 position이 7이라는 값을 가짐

인덱스는 0~6까지임

2. 2바이트를 버퍼에 저장하면 2바이트는 position이 위치한 0인덱스에서 시작해 버퍼에 저장.

    0,1 인덱스에 저장 => position은 2번 인덱스가 됨.

3. 이어서 3바이트를 저장하면 3바이트는 position 2인덱스에서 시작해 버퍼에 저장.

   2,3,4 인덱스에 저장되고 position은 5번 인덱스가 됨.

4. 버퍼에 저장된 바이트를 읽으려면 flip() 메서드 호출해야함. flip()을 호출하면 limit을 현재

   position 5 인덱스로 설정하고 position을 0인덱스로 설정함.

5. 버퍼에서 3바이트를 읽는다고 하면 position이 0이므로 처음 0,1,2  3바이트가 읽어지고 

   position은 3번 인덱스로 이동

6. position이 3번 인덱스를 가르키고 있을 때 mark()를 호출하여 현재 위치를 기억시켜 놓음.

   따라서 mark는 3번 인덱스에 위치

7. 이어서 2바이트를 더 읽으면 position은 5 인덱스로 이동.

8. position을 mark 위치로 다시 이동시킨다고 하면 reset()메서드를 호출하면 position은 mark가 있는

   3번 인덱스로 이동 / *mark가 없는 상태에서 reset()호출 시 InvalidMarkException 발생

9. 버퍼를 되감아 동일한 데이터를 한 번 더 읽고 싶다면 rewind()메서드를 호출하여 

   limit은 변하지 않지만 position은 다시 0번 인덱스로 설정됨.

   mark는 position이나 limit이 mark 보다 작은 값이 되면 자동적으로 사라짐.

10. rewind() 대신 clear()을 호출하면 Buffer의 세 가지 속성은 초기화됨

    limit은 capacity로 position은 0으로 mark는 자동적으로 사라짐. * 데이터는 삭제되지 않음.


* 버퍼의 위치 속성을 변경하는 compact() 메서드도 있음. compact() 호출하면 현재 position에서

  limit 사이의 데이터가 0번 인덱스로 복사되고 현재 position은 복사된 데이터 다음 위치로 이동


이상입니다.


반응형