본문 바로가기
프로그래밍/Java

자바의 NIO

by Daniel.kwak 2018. 12. 5.

기존의 자바I/O 는 느리다는 편견이 존재했다. Blocking 방식 때문이었을까. 

  • FileReader
  • PrintReader
  • FileWriter
  • PrintWriter

위와 같은 클래스는 쓰기 어려움은 없이 I/O 작업은 할 수 있었지만, 커널 버퍼를 직접 접근하는 Direct Buffer를 핸들링 할 수 없었다. 소켓이나 파일에서 Stream이 들어오면 커널 버퍼에 쓰여지게 되는데, 자바 코드에서 이를 접근할 수가 없었기 때문. 커널에서 JVM 내부로 옮겨와야 하는 오버헤드로 인해 속도 이슈가 있었다. 세부적인 이유는

  • JVM 내부로 복사할 때 CPU 사용(디스크에서 커널버퍼로 복사하는 과정은 CPU가 하지 않고 DMA가 해준다.) 
  • 복사 Buffer 사용 후 GC를 통해 정리(GC의 대상이 된다.)
  • 쓰레드 Blocking 방식으로 I/O작업 진행 (커널버퍼에서 JVM 내부 메모리로 복사하는 동안 다른 작업을 할 수 없다)
->사용자가 USB를 꽃고 파일 입출력을 기다리다가, 완료가 됐다는 메세지를 받았음에도 USB를 빼면 파일의 용량은 0kb 였음.
->혹은 중간에 USB를 빼면 어떻게 대처할 것인가.

NIO에서는 이러한 문제점들을 어떻게 해결하였는지 살펴보자. 
NIO에서는 커널 버퍼에 직접 접근할 수 있는 Buffer 클래스를 제공한다. 내부에서 커널 버퍼를 직접 참조하고 있다. 


java nio buffer에 대한 이미지 검색결과


많은 Buffer 클래스 중, ByteBuffer 클래스만 Direct Buffer를 지원한다. 즐, 커널 버퍼에 직접 접근할 수 있는 NIO의 장점을 이용하기 위해서는ByteBuffer의 allocateDirect()라는 메소드를 이용해서 ByteBuffer를 만들어야 함. 


ByteBuffer buffer1 = ByteBuffer.allocate(10); // 일반 버퍼이다.
ByteBuffer buffer2 = BytBuffer.allocateDirect(10); //커널 버퍼를 직접 다루는 버퍼


Buffer를 조금 자세히 살펴보자.

Buffer에는 네 가지 포인트가 있다. 

  • Position : 현재 읽을 위치나 현재 쓸 위치를 가르킨다. ByteBuffer에서 get() 함수로 읽기를 시도할 경우 positoin위치부터 읽으며, put() 메소드로 쓰기를 시도해도 마찬가지이다. write 작업이 발생하면 position은 한 칸 이동한다.
  • limit : 현재 ByteBuffer의 유효한 쓰기 및 읽기 위치를 나타낸다. -> 이 버퍼는 여기까지 읽을 수 있다"의 의미이다. 
  • Capacity : ByteBuffer의 용량을 나타낸다. 그러므로 항상 끝 위치를 참조하고 있다. 위치를 바꿀 수 없다.
  • mark : 개발자에게 편리한 포인터. 위치를 기억하고 있다가 필요할 때 사용할 수 있다.

NIO의 Channel
기존 I/O에서는 파일을 읽고 쓸 때 스트림 기반이었다. 따라서 입력 스트림, 출력 스트림을 만들어야 했지만 NIO에서는 Channel을 통해 양방향으로 read, write를 동시에 할 수 있다. Channel은 public 생성자가 따로 없고, 기존 스트림의 getChannel()을 통해서 객체를 얻는다. 
여기서는 FileChannel을 알아본다.
FileChannel은 Blocking만 가능하며, 이는 Selector와 관련이 깊지만 여기서는 다루지 않는다. FileChannel은 File내용을 ByteBuffer로 불러오거나, ByteBuffer에 있는 내용을 File에 쓰는 역할을 한다. 간단하게 코드로 살펴보자. 


FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStrream("output.txt");

ByteBuffer buffer = ByteBuffer.allocateDirect(10);
FileChannel cin = fis.getChannel();

FileChannel cout = fos.getChannel();
cin.read(buf); //Channel 에서 읽어서 buf에 저장

buf.flip();
cout.write(buf);


read 메소드를 통해 position에서부터 limit 위치까지 내용을 FileInputStream의 내용으로 채운다. write 동작도 마찬가지.

한 가지 주의해야할 사항은, inputStream으로 만든 FileChannel로 write 동작을 진행하면 Exeption이 발생한다는 것. 반대의 경우도 마찬가지.

다만, RandolAccessFile같은 클래스는 seek으로 탐색한 파일 포인터 위치에서 읽거나 쓸 수 있는 객체이므로 read/write 모두 수행 가능하다.


짚고 넘어가야할 메소드는 flip이다. flip은 ByteBuffer에 저장한 후 그 데이터를 읽기 위해서 반드시 써줘야 한다. limit를 현재 positoin으로 설정 후, position을 0으로 설정하는 함수이다. position의 위치에서부터 읽고 쓰기를 하기 때문에 flip은 적절하게 잘 써줘야 한다.

'프로그래밍 > Java' 카테고리의 다른 글

익명 클래스(Anonymous Class)  (2) 2019.07.21
String vs StringBuffer vs StringBuilder  (0) 2019.02.16