기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다. 하지만 jdk1.3부터는 Java IO의 한계를 보완한 Java NIO를 사용하여 I/O에서 속도 향상을 낼 수 있습니다. 그러나 NIO의 사용법은 기존 I/O와는 매우 달라 배우기가 생각만큼 쉽지는 않습니다. 이번 포스팅에서는 Java NIO에 대해 알아보고, 예제를 통해 FileHandling의 Performance를 향상시키는 간단한 예제를 다뤄 NIO에 쉽게 접할 수 있도록 하겠습니다. 생각보다 길어져서 포스팅을 세 개로 나누겠습니다.

차근차근 포스팅하도록 하겠습니다. 제가 미흡한 점이 많습니다. 혹시 내용상 오류나 오탈자를 발견하신 분은 바로 댓글로 태클 걸어주시면 감사하겠습니다^^

1. 파일 큐 (File Queue)

자료구조에 나오는 큐(queue)에 대해선 잘 아실겁니다. 선입선출의 특성을 가지는 Collection이죠. 다양한 방법으로 구현이 가능합니다. 이번 포스팅에서 고려하고자 하는건 파일 큐(File Queue)입니다. 말 그대로 큐에 들어가는 내용을 파일에 넣자는 거죠.

파일 큐(File Queue)는 파일에 내용을 저장하는 큐를 의미한다!

image0

일반적으로 메모리상에 내용을 저장하는 것이 속도가 빠르다는 것은 자명한 사실입니다.  파일 입출력은 당연히 메모리 입출력보다 느리지요. 하지만, 메모리는 휘발성이라는 위험부담이 있습니다. 빠르면 좋겠지만, 시스템의 다운에도 큐에 저장하는 내용이 매우 중요한 경우에 파일 큐를 쓰게 됩니다.

  • 파일 큐는 메모리 큐보다 느리다.
  • 파일 큐는 시스템의 다운에도 안전하지만 메모리 큐는 시스템이 다운되면 큐에 쌓인 내용이 날아가 버릴 위험성이 있다. (메모리는 휘발성, 파일은 안전함)
  • 시스템 다운에도 내용이 보전되어야 할 중요한 자료를 저장하는 큐는 파일 큐로 만든다. ex) 은행에서 금전 거래에 대한 이벤트 정보 (이벤트 큐에 넣겠죠) 그 외에도 많은 곳에 적용 될 수 있습니다. 어쨋든 큐는 요청과 처리를 비동기적으로 처리하면서 속도를 올릴 때 꼭 필요한 방법이니까요.

위의 문제 때문에 파일 큐가 필요하다는 것은 자명한 사실입니다.

2. 파일 큐 (File Queue)를 구현하기

파일큐의 구현 방법을 아주 간단히 설명하겠습니다. 일단 파일 큐는 다음 인터페이스를 구현할 생각입니다.

public interface ByteFileQue {
    public boolean open() throws IOException
    public int put(byte[] srcBuf);
    public byte[] get() throws IOException;
    public byte[] peek();
    public int size();
    public boolean close();
    public boolean isClosed();
}

open()은 파일을 오픈, close()는 파일을 닫는 메서드입니다. put(), get() 함수는 각각 바이트 배열을 큐에 넣고 빼는 메서드입니다. 간단한 인터페이스죠.

각각 ByteFileQue를 구현할 두 클래스가 있습니다. 각각 SimpleByteFileQue, NIOByteFileQue 라고 이름을 짓겠습니다. 보시면 당연히 아시겠지만, SimeByteFileQue는 일반 java I/O로 구현한 파일 큐이고, NIOByteFileQue는 NIO로 구현한 파일 큐입니다. 두가지 모두 RandomAccessFile을 이용하여 구현 하면 됩니다. RandomAccessFile을 NIO를 이용하여 입출력하는 방법은 이전 포스팅을 참고합시다.

파일의 맨 앞부분에 헤더를 만듭니다. 헤더에는 tail, haed의 파일 포인터 위치와 size에 대한 위치를 저장합니다. 따라서 get할때에는 tail에 대한 위치를 읽어 seek(tail) 과 같은 방법으로 파일포인터를 이동한후 정해진 길이만큼 파일을 읽어 반환을 하면 됩니다. 한 가지 주의할 점은 반드시 ByteBufferallocateDirect를 이용하여 할당해야 한다는 점입니다.

  • 자바상에서 DMA의 도움을 받을 수 있는 Direct Buffer를 사용하려면 ByteBuffer를 사용하여야 함!
  • ByteBuffer.allocateDirect()메소드를 사용해야 Direct Buffer가 생성됨!

쓰레드간 동기화 부분도 고려해야 되는데, synchronized 블록이나 synchronized 메서드를 사용하는 방법과, ReentrantLock을 사용하는 방법이 있습니다. 테스트 해보았는데, 두 방법 모두 비슷한 퍼포먼스를 내므로, 둘 중 아무 방법을 사용하셔도 무방합니다. 하지만 [ReentrantLock][1]은 프로세스간 동기화를 유지시켜주는 FileLock과 함께 사용할 수 없습니다. 따라서 synchronized 블록 혹은 synchronized 메서드로 동기화 하는 방법을 추천해드립니다.

FileLock은 jdk1.4부터 지원되는 클래스입니다. 기존 jdk에서는 JVM상 돌아가는 프로세스간 파일 동기화를 지원해주지 않았습니다. 때문에, 따로 파일을 둬서 파일을 읽는 중인지 혹은 그렇지 않은지를 표시하여 동기화를 유지하였다고 합니다. 매우 불편한 방법이죠. 하지만 이제는 자바에서 FileLock으로 매우 쉽게 프로세스간 동기화를 구현할 수 있습니다. 다만 FileLock은 쓰레드간 동기화를 유지시켜주지는 않습니다.

자세한 것은 각자 구현해 보도록 합시다.

2. NIO 파일 큐와 일반 파일 큐의 속도 차이 비교하기

생각보다 확연하게 차이가 납니다. 속도 차이를 나타내는 자료를 봅시다.

아래 자료는 1분간 get 메서드와 put 메서드가 몇개의 자료를 넣고 뺄 수 있는지에 대한 수행 자료입니다. NIO쪽이 월등히 많다는 것을 볼 수 있습니다. 일반 FileQueue는 put 함수를 반복하여 수행했을때 1분동안 38만건 정도 put할 수 있었고, Nio FileQueue는 130만 건 정도 수행 할 수 있었네요. 엄청나게 월등하죠.

image1

아래 자료는 각각 Thread를 두개를 돌려 한 쪽 쓰레드에선 Queue에 자료를 넣고, 다른 쪽 큐에서는 자료를 꺼내는 작업을 반복시킨 것입니다. 따라서 쓰레드간 동기화시 Blocking 되는 overhaed까지 포함된 결과 입니다. 넣고 꺼내는 자료는 100개부터 1,000,000개 까지 수행하였습니다. 걸리는 시간의 단위는 밀리새컨드 입니다. 위에서 put/get만 수행할 때만큼 속도 차가 나지는 않죠. 이것은 Thread 동기화시 blocking 현상때문입니다. 속도가 빠른만큼 blocking 될 확률이 높죠. 그럼에도 불구하고 NIO가 여전히 월등한 퍼포먼스를 보여줍니다.

image2

보시는 바와 같이 NIO가 월등히 좋은 퍼포먼스를 내는 것을 보실 수 있습니다.