본문 바로가기
JAVA

[NIO] 1부, 버퍼와 채널 #1

by windrises 2007. 3. 29.

자바2 1.4에는 기존해 확장 API로 존재했던 보안 관련 API를 비롯하여 다양한 API가 포함되었으며, 정규표현식이나 로깅, 설정과 같이 어플리케이션을 개발하는 데 필요로 하는 다양한 기능들이 새롭게 추가되었다. NIO(New IO) API는 이렇게 1.4 버전에 새롭게 추가된 API 중의 하나인데, 반드시 알고 있어야 할 매우 중요한 API라 할 수 있다.

NIO API는 자바 1.3 버전까지 사용해왔던 기존 IO API와는 비교가 안될 정도로 성능, 확장성 등에서 뛰어나게 설계되어 있다. 특히 논-블럭킹(Non-blocking) 입출력과 데이터 버퍼링 기능을 사용하여 기존 버전에서는 상상도 할 수 없을 정도의 뛰어난 성능을 지닌 서버 프로그램을 개발할 수 있게 되었다.

본 글에서는 NIO API를 구성하고 있는 네 가지 기본 요소에 대해서 살펴보고, 네 가지 요소 중 버퍼와 채널에 대해서 살펴보도록 하자.

NIO API의 네 가지 핵심 요소

NIO API는 Buffer, Charset, Channel 그리고 Selector의 네가지 핵심 요소로 구성되어 있는데, 이들 네 요소는 각각 다음과 같은 기능을 제공한다.

  • Buffer - 버퍼를 나타낸다. 기본 데이터 타입에 대한 버퍼가 각각 존재하며 입출력 데이터를 임시로 저장할 때 사용된다.
  • Charset - 캐릭터셋을 나타낸다. 바이트 데이터와 문자 데이터를 인코딩/디코딩할때 사용된다.
  • Channel - 데이터가 통과하는 스트림을 나타낸다. 소켓, 파일, 파이프 등 다양한 입출력 스트림에 대한 채널이 존재한다.
  • Selector - 하나의 쓰레드에서 다중의 채널로부터 들어오는 입력 데이터를 처리할 수 있도록 해 주는 멀티플렉서(multiplexer)이다. 논블럭킹 입출력을 위한 핵심 요소이다.
Buffer

Buffer는 byte, char, int 등 기본 데이터 타입을 저장할 수 있는 저장소로서, 배열과 마찬가지로 제한된 크기(capacity)에 순서대로 데이터를 저장한다. Buffer는 데이터를 저장하기 위한 것이지만, 실제로 Buffer가 사용되는 것은 채널을 통해서 데이터를 주고 받을 때이다. 채널을 통해서 소켓, 파일 등에 데이터를 전송할 때나 읽어올 때 버퍼를 사용하게 됨으로써 가비지량을 최소화시킬 수 있게 되며, 이는 가비지 콜렉션 회수를 줄임으로써 서버의 전체 처리량(throughput)을 증가시켜준다.

NIO API는 다음과 같이 모든 버퍼가 상속받아야 할 한개의 추상 클래스와 8개의 Buffer를 제공한다.

java.nio.Buffer 모든 버퍼가 상속받는 추상 클래스
java.nio.ByteBuffer byte 타입의 데이터를 저장하는 버퍼. 다이렉트(direct) 버퍼와 논다이렉트(nondirect) 버퍼가 존재하며, ReadableByteChannel과 WritableByteChannel을 통해서 데이터를 입출력할 수 있다.
java.nio.MappedByteBuffer byte를 저장하는 버퍼로서 항상 다이렉트이다. 파일의 특정 영역을 메모리에 매핑시킬 때 사용된다.
java.nio.CharBuffer char를 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다. 채널에 쓸 수 없다.
java.nio.DoubleBuffer double을 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다. 채널에 쓸 수 없다.
java.nio.FloatBuffer float을 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다.
java.nio.IntBuffer int 데이터를 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다.
java.nio.LongBuffer int 데이터를 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다.
java.nio.ShortBuffer int 데이터를 저장하며, 다이렉트 또는 논다이렉트 버퍼일 수 있다.

위 표를 보면 boolean을 제외한 나머지 기본 데이터 타입에 대한 Buffer가 존재하는 것을 알 수 있으며, MappedByteBuffer를 제외한 나머지 버퍼들은 다이렉트이거나 논다이렉트 버퍼일 수 있다는 것도 확인할 수 있다. 다이렉트/논다이렉트 버퍼에 대해서는 잠시 뒤에 살펴볼 것이며, 여기서는 먼저 버퍼의 속성인 용량(capacity), 제한크기(limit), 위치(position)에 대해서 살펴보도록 하자.

다이렉트냐 논다이렉트냐에 따라서 다르지만, 추상적으로 Buffer는 배열과 비슷한 형태로 데이터를 저장하고 있다고 생각하면 된다. 이때 Buffer는 배열과 마찬가지로 포함할 수 있는 데이터의 총 크기를 갖고 있으며, 이를 용량(capacity)라고 한다. 또한, Buffer에서 다음에 읽거나 쓰는 부분을 위치(position)라고 한다. 대부분의 버퍼는 다이렉트 버퍼를 생성할 수 있는 메소드인 allocateDirect() 메소드를 제공하는데, 만약 다음과 같이 ByteBuffer를 생성했다면 그림1과 같이 byte 버퍼가 생성될 것이다.

  ByteBuffer buf = ByteBuffer.allocateDirect(8);


그림1 - 버퍼 생성시 용량, 제한크기, 위치의 초기값

그림1을 보면 초기 제한크기는 버퍼의 용량과 동일한 것을 알 수 있다. 일단 버퍼를 생성하면 버퍼에 데이터를 삽입할 수 있게 된다. 모든 Buffer 클래스는 데이터를 삽입할 때 사용되는 putXXX() 형태의 메소드를 제공하고 있다. 예를 들어, ByteBuffer의 경우는 putByte(byte b), putChar(char c) 등 다양한 데이터 타입을 바이트로 저장할 수 있는 메소드를 제공하고 있으며, 다른 Buffer 클래스들 역시 각각 알맞은 메소드를 제공하고 있다.

Buffer에 데이터를 저장하면 위치는 저장한 만큼 뒤로 이동하게 된다. 예를 들어, 다음과 같이 3 바이트의 데이터를 저장했다고 해 보자.

  buf.putByte( (byte)0xAB );
  buf.putShort( (short)0xCDEF );
  

이 경우 그림2와 같이 위치값이 3으로 변경된다.


그림2 - 데이터를 삽입한 이후의 위치값 변화

Buffer의 위치값이 제한크기(limit)와 같아지면 더 이상 Buffer에 데이터를 삽입할 수 없게 된다. 제한크기의 값은 limit(int newlimit) 메소드를 사용하여 변경할 수 있으며, 만약 위치가 제한크기까지 도달한 상태에서 데이터를 삽입하려고 하면 java.nio.BufferOverflowException이 발생할 것이다. 비슷하게 제한크기 이후에 위치한 데이터를 읽어오려고 할 경우에는 BufferUnderflowException이 발생하게 된다. 즉, 제한크기는 실제 버퍼의 크기인 용량에 상관없이 사용자가 사용할 수 있는 가상의 버퍼 제한 영역을 표시한다고 생각하면 된다.

원하는 만큼 버퍼에 데이터를 삽입하는 이유는 삽입한 데이터를 사용하기 위해서이다. 각각의 Buffer 클래스는 데이터를 참조할 수 있는 메소드인 getXXX() 형태의 메소드를 제공하고 있다. 하지만, get() 메소드를 사용하거나 채널에 버퍼에 저장된 데이터를 출력하기 위해서는 먼저 위치값을 읽어올 데이터의 인덱스로 변경해야 하고 제한크기를 알맞게 변경해주어야 한다. 예를 들어, 그림2의 경우는 처음부터 데이터를 읽어오기 위해서는 위치값을 0으로 변경해주어야 하며, 또한 저장한만큼의 데이터를 읽어오기 위해서는 제한크기를 3으로 변경해주어야 한다.

위치값과 제한크기는 position(int) 메소드나 limit(int) 메소드를 사용하여 변경할 수 있지만, flip() 메소드를 사용하여 변경할 수도 있다. flip() 메소드는 제한크기를 현재 위치값으로 변경하고 난 후 위치값을 0으로 초기화해준다. 예를 들어, 그림2 상태에서 flip() 메소드를 호출하면 그림3과 같이 위치와 제한크기의 값이 변경된다.


그림3 - flip() 메소드 호출 후 위치값과 제한크기의 변화

flip() 메소드를 실행하면 버퍼의 처음부터 데이터를 읽어올 수 있게 되며, 또한 채널을 통해서 버퍼에 있는 데이터를 모두 출력할 수 있게 된다.

flip() 메소드 뿐만 아니라 Buffer는 rewind() 메소드와 clear() 메소드를 제공해주고 있다. rewind() 메소드는 위치값을 0으로 변경해주어 버퍼의 처음부터 사용할 수 있도록 해 주며, clear() 메소드는 위치값을 0으로 제한크기를 용량으로 변경해주고 버퍼의 내용을 비워주어 버퍼를 초기 상태로 만들어준다.

Direct vs Nondirect

Buffer에는 다이렉트 방식과 논다이렉트 방식이 존재한다. 다이렉트 방식의 Buffer는 연속된 메모리 블럭을 할당하며 원시 접근 메소드를 사용하여 메모리 블럭의 데이터를 읽고 쓴다. 반면에 논다이렉트 방식의 Buffer는 자바의 배열을 데이터 저장소로 사용한다.

ByteBuffer의 경우 allocateDirect() 메소드를 사용하여 다이렉트 버퍼를 생성하는데, 다이렉트 버퍼를 생성할 때에는 일반적인 자바 배열을 사용하는 경우 메모리를 할당하고 해제할 때 논다이렉트 버퍼에 비해 더 많은 시간이 소비되는 반면에 원시 메소드를 사용하기 때문에 입출력 처리 속도는 빠르다. 따라서, 다이렉트 버퍼의 경우는 크고 지속적으로 사용되는 버퍼에 알맞다.

논다이렉트 버퍼의 경우는 wrap() 메소드를 사용하여 생성한다. 예를 들어, ByteBuffer의 경우는 다음과 같은 방법으로 논다이렉트 버퍼를 생성한다.

   byte[] buff = new byte[512];
   ByteBuffer nonDirect = ByteBuffer.wrap(buff);

논다이렉트 버퍼는 자바의 배열을 사용하기 때문에 원시 메소드 호출이 이루어지지 않으며 자바의 기본적인 배열 접근 방식을 통해서 버퍼를 관리하게 된다. 논다이렉트 버퍼의 경우는 다이렉트 버퍼에 비해 메모리를 할당하거나 해제하는 시간이 적게 들지만, 입출력 속도는 원시 메소드를 사용하는 것보다 느리다. 따라서, 논아디렉트 버퍼는 임시적으로 사용하고자 할 때 주로 사용된다.

-출처 [http://javacan.madvirus.net/]