Java NIO 기반 입출력 관련 정리 from 이것이 자바다(책)

Updated:

NIO

NIO : New Input/Output 이라는 뜻이다. 우선 IO와 차이점을 비교를 해보자

IOandNIO

입출력과 버퍼의 방식이 우선 다르다. 그리고 넌블로킹의 지원을 해주는지에 대해서 차이점을 보이고 있다.

스트림은 입력 스트림과 출력 스트림으로 구분이 되어있어, 입 출력 둘다 관련한 작업이 필요한 경우, 각각 따로 객체를 생성하여 로직을 짜야 하지만, 채널은 양방향으로 입출력이 가능하다.

버퍼의 경우는 스트림에 비해, 복수 개의 바이트를 한꺼번에 입출력하므로, 빠른 성능을 가지고낼수있다.

넌블로킹의 경우는 말 그대로 입출력 작업시 스레드가 블로킹 되지 않는다는것을 의미한다. 블로킹은 데이터를 받기전까지 현재 진행되고있는 프로세스의 과정들은 진행될수가 없다. 그러나, 넌블로킹의 경우는 우선은 바로 즉시된 값을 리턴을 받으며, 지속적으로 호출된 함수를 계속 보내며, 호출된 함수가 완료되면 데이터를 리턴을 받는데, 이것을 polling이라고 한다.

넌블로킹이 동작하는 과정은 다음 그림과 같다.

Nonblock

출처 : https://ozt88.tistory.com/20


그런데 입출력과 관련된 버퍼가 2가지 형태로 나뉜다. 넌다이렉트(non-direct)다이렉트(direct)의 형태로 나뉜다. 두 버퍼의 특징은 다음과 같다.

Buffer

다이렉트 버퍼는 버퍼 생성 시간이 느리다보니, 한번 만들어놓고 재사용하는것이 적합하다. 그러나 넌다이렉트 버퍼의 경우 입출력을 하기 위해, 임시 다이렉트 버퍼를 생성하고 넌다이렉트 버퍼에 있는 내용을 임시 다이렉트 버퍼에 복사를 하게 된다. 그러고 나서, 임시 다이렉트 버퍼를 사용해 운영체제의 native I/O 기능을 수행한다. 그러다 보니, 입출력 성능이 상대적으로 떨어지게 된다.

그리고 넌다이렉트의 경우는 컴퓨터의 성능과 현재 상태에 따라서 버퍼 크기가 유동적이기 때문에, 최대 버퍼의 크기를 알수 없으니, 테스트를 해보면서 하길 바란다.

ByteBuffer directBuffer = ByteBuffer.allocateDirect(200*1024*1024);
System.out.println("다이렉트 버퍼 생성");

ByteBuffer nonDirectBuffer = ByteBuffer.allocate(200*1024*1024);
System.out.println("넌 다이렉트 버퍼 생성");

그리고 데이터를 처리할때 바이트 처리를 하는 순서는 운영체제마다 차이가 있다. 간단하게 앞쪽 바이트 부터 처리를 하는것을 Big Endian, 뒤쪽 바이트부터 처리를 하는것을 Little Endian이라고 부른다. 언어의 경우, Java가 Big Endian이고, C#이 Little Endian의 기법을 지니기도 한다. 하지만 본인이 그러한 것을 잘 모른다고 가정을 했을때는, 다음과 같은 테스트코드를 입력하여 확인을 해보도록한다

System.out.println("운영체제 종류 : "+System.getProperty("os.name"));
System.out.println("네이티브의 바이트 해석 순서 : "+ByteOrder.nativeOrder());

만약, Little Endian에서 동작하는 데이터 파일이 Big Endian으로 동작하는 운영체제에서 읽을때는, Byte Order 순서를 변경해줘야 한다. 왜냐하면, 다이렉트 버퍼의 경우는 운영체제의 native I/O를 사용하므로 운영체제의 기본 해석 순서로 JVM의 해석 순서를 맞추는것이 성능 향상에 도움이 되기 때문이다.

(넌 다이렉트의 경우도 결국, 임시다이렉트 버퍼를 만들어서, 그 임시 다이렉트 버퍼가 native I/O를 사용하니까, 결국 버퍼의 종류와 상관 없이 Byte Order순서를 변경해주는 것이 성능 향상에 도움을 줄 수 있다고 필자는 해석을 했습니다. 잘못 알고있는거면 가르침 부탁드립니다)

그래서 다음과 같은 코드로 Byte Order를 맞춰주면 된다.

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(100).order(ByteOrder.nativeOrder());

파일을 이제 비동기적으로 받기 위해선 파일 비동기 채널을 만들어서 작업을 처리 해야 한다. 자바에서는 AsynchronousFileChannel을 제공한다. 처음에는 open메소드를 이용하여 채널을 생성한다.

AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(
	Path file,
	Set<? extends OpenOption> options,
	ExecutorService executor,
	FileAttriebute<?>... attrs
);

위의 코드는 스레드풀을 개발자가 직접적으로 만들었다고 가정을 한것이다. 그다음은 read나 write메소드를 통해서 읽고 쓸 수 있다.

read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer,A> handler);
write(ByteBuffer src, long position, A attachment, CompletionHadnler<Integer,A> handler);

여기서, dst나 src는 읽거나 쓰기 용도인 ByteBuffer이고, position은 읽거나 쓸 위치이다. 그래서 0이라고 설정하면, 처음부터 읽거나 쓰게 된다. attachment는 콜백 메소드로 전달할 첨부 객체이다. 즉, 여기서 Integer값(CompletionHandler<Integer,A>)은 read하거나 write를 했을때, 쓴 바이트 수 이며, 첨부 객체(attachment)는 read하거나 write를 했을때, 추가적으로 정보를 얻을 수 있게 된다. 즉, result값외에, A타입의 정보를 추가적으로 얻겠다는 의미라고 보면 된다.

우선 TCP 넌블로킹 채널와 비동기 서버소켓 채널에 대해서는 생략을 하도록하겠습니다.(Java FX로 코드들이 위주로 되어있고, 메소드들이 deprecated되었는지 실습을 해보지 못했습니다)


마지막으로 UDP 채널은 DatagramChannel을 사용할수 있다. 책에서 블로킹 방식으로만 설명이 되어있기 때문에, 블로킹 방식을 통해 설명하겠다. Udp는 이전에 설명했다시피, TCP와는 다르게 따로 handshake을 하는 과정이 필요하지 않습니다. 그래서 DatagramChannel을 만들고(open) 보내고 받으면 됩니다(send, receive). 다음과 같은 메소드들을 이용하여 정보를 주고 받을수 있습니다.

DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET);
int byteCount = datagramChannel.send(byteBuffer,new InetSocketAddress("localhost",5001));
// ("localhost",5001) -> ip와 port번호
SocketAddress socketAddress = datagramChannel.receive(ByteBuffer dst);
datagramChannel.close();

위와 관련한 모든 예제들은 아래 링크 주소에 있으니 참고해주시면 감사하겠습니다.
https://github.com/goodgood619/ThisIsJava

Leave a comment