[Java] NIO에 대해서

devdo·2022년 1월 23일
1

Java

목록 보기
37/59

자바 기존 IO와 New IO에 대해 알아보겠습니다.

과거 IO에 때문에 자바가 느리다는 인상을 많이 줬었습니다. 그런 문제를 극복하기위해 Non-blocking IO API를 제공하므로써 자바는 극복해왔는데 Blocking, Non-Blocking부터 정리해보겠습니다.

Blocking, Non-Blocking

서버관련 공부를 하다보면 비동기 논블록킹 IO에 대한 언급이 종종 나옵니다. NodeJS의 장점은 이벤트기반의 비동기 논블록킹 IO지원으로 자원을 효율적으로 사용한다는 장점을 내세우죠. 이 부분도 다시 언급해보겠습니다.


Blocking API

Blocking API란 API를 호출한 Thread가 API의 작업이 끝날 때까지 다른 동작을 하지않는 API를 블록킹이라고 합니다.

흔히 우리가 사용하는 Java의 기본 IO관련 API들은 Blocking 방식으로 사용되어왔습니다.

InputStream, OutputStream을 확장하거나 직접 사용하는 클래스들이 대표적인 예입니다.

InputStream inputstream = new FileInputStream("c:\\data\\input-text.txt");

int data = inputstream.read(); // Blocking!
while(data != -1) {
  doSomethingWithData(data);
  data = inputstream.read();
}
inputstream.close();

위 코드처럼 Blocking API들은 반환값을 받을 때까지(작업이 끝날 때) Blocking 되기 때문에 해당 Thread는 idle상태로 유지되게 됩니다. 예시는 파일 IO이기에 큰 delay가 없더라도 Network IO는 CPU사이클에 비해 충분히 오래 걸리기 때문에 bottle neck의 원인이 되기 쉽습니다.

이러한 문제를 해결하려면 Non-Blocking API를 사용할 필요가 있습니다.


Non Blocking API

Non-Blocking API는 쉽게 말해 API호출시 요청한 작업의 완료 여부와 상관없이 즉각적으로 현재 상태에 대한 응답이 옵니다. 그래서 API 호출 후 Thread 제어권이 있기 때문에 다른 작업을 진행할 수 있습니다.

Java 의 NIO(New IO)의 등장으로 Java에서 Non Blocking 방식을 구현할 수 있게 되었습니다.

하나의 Thread가 IO작업에 의존적이지 않기 때문에 하나의 Thread로 다수를 IO를 처리할 수도 있습니다.

API의 작업이 끝나고 결과를 어디서 처리하느냐에 따라 Asyncronous거나 Synchronous 방식으로 나뉘어집니다.

위 개념들은 여러 어플리케이션에서 성능과 직결되기 때문에 중요하게 다루어집니다.

예를들어 Client Side Application를 개발시 Asyncronous non-Blocking 방식을 잘이해하고 있어야지 외부 API요청이 UI Thread의 랜더링에 방해하지않습니다.

아주 간단히 개념정도만 알고 Java NIO, IO에 대해서만 더 정리해보겠습니다.


IO 와 NIO 의 주요 차이점

NIO와 IO는 개념적으로는 Blocking, Non-Blocking의 차이가 있지만 그 이외에도 차이점이 존재합니다.

비교할 요소는 아래 3가지입니다.

  • Stream Oriented vs Buffer Oriented
  • Blocking IO vs Non Blocking IO
  • Selector

Stream 기반 vs Buffer 기반

Java IO와 NIO 사이의 큰 차이는 IO는 Stream 기반이지만, NIO는 Buffer 기반이라는 것입니다.

이것이 의미하는 바는 아래와 같습니다.

스트림 기반의 Java IO스트림으로 부터 한번에 여러 바이트를 읽습니다. 읽은 바이트를 가지고 무엇을 할지는 사용하는 사람에 달려있습니다. 데이터는 어디에도 캐시되어 있지않습니다. 더구나 스트림 속 데이터에서 앞 뒤로 이동할 수 없습니다. 만약 스트림으로 부터 읽은 데이터 내부에서 앞뒤로 이동할 필요가 있다면, 버퍼를 만들어 캐싱을 해야합니다.

버퍼기반의 Java NIO는 조금 다릅니다. 이미 처리된 buffer로부터 데이터를 읽습니다. 만약 필요하다면 버퍼 내부에서 앞뒤로 이동할 수 있습니다. 이러한 특징은 데이터를 처리하는 동안 좀 더 유연함을 제공합니다.

그러나 데이터를 완벽히 처리하기 위해서 필요한 데이터가 모두 버퍼에 있는지 체크할 필요가 있습니다.

그리고 버퍼에서 더 많은 데이터를 읽을 때, 버퍼 속에서 아직 전처리되지 않은 데이터를 사용하지 않도록 확실히 해야합니다.(그래서 flush()필요)


Blocking vs Non-blocking IO

Java IO의 다양한 스트림들은 Blocking 방식입니다. 하나의 thread가 read() or write()를 발생시킬때, 해당 thread는 데이터를 읽을 때까지 혹은 데이터를 적을때까지 blocked. 막혀있습니다.

Java NIO의 Non-Blocking mode는 thread가 channel을 통해 데이터를 읽는 것을 요청하고 오직 현재 이용가능한 데이터만을 얻을 수도 있고 만약 이용할 수 있는 데이터 없다면 아무것도 돌려주지 않는 동작을 즉시 할 수 있게합니다. (즉 스레드가 Blocking 되지않습니다.)

데이터가 읽을 수 있는 상태가 될 때까지 block된체 남아있기보다는 thread는 그 때 다른 무언가를 할 수 있습니다.

Non-blocking writing 도 마찬가지입니다. thread는 channel을 통해 어떤 데이터를 쓰도록 요청하고 데이터를 전부 다 작성할 동안 기다리지않습니다. 작성을 하는 동안 그 thread는 즉시 다른 동작을 할 수 있습니다.

thread들이 block 되지않는 IO 호출을 할 때 주로 다른 channel에 IO을 수행합니다. 이러한 점에서 보면 단 하나의 thread는 다수의 input, output channels을 관리할 수 있습니다.


Selector

Java NIO Selector는 하나의 thread가 여러 개의 input channel들을 모니터링할 수 있습니다. 이러한 특징은 다수의 Thread로 IO를 관리하는 방식에 비해 Thread Switching을 줄이기 때문에 이점을 줍니다.

하나의 selector를 사용해서 다수의 channels를 등록할 수 있으며 하나의 스레드를 사용해서 input을 처리할 수 있는 channel을 선택할 수 있으며 또한 writing을 위해 준비된 channel을 선택할 수 있다.

이런 selector machanism은 하나의 thread가 여러개의 channel을 쉽게 관리할 수 있게 해준다.


NIO와 IO가 어플리케이션 디자인에 끼칭는 영향

IO toolkit으로 NIO를 선택할 지 혹은 IO를 선택할 지는 다음과 같은 어플리케이션 디자인 측면에 영향을 준다.

  • The API calls to the NIO or IO classes.
  • The Processing of data
  • The number of thread used to process the data

API Calls

물론 IO를 사용할 때와 NIO를 사용할 때 API는 보이기에 당연히 다릅니다. InputStream으로 부터 byte 데이터를 읽기보다는 buffer속에서 데이터를 읽어야합니다.


The Processing of Data

IO 를 사용할 때 우리는 InputStream 이나 Reader에서 byte를 읽습니다.
line 단위의 문자열 데이터의 스트림을 처리한다고 상상해보자.

InputStream input = ...; // get the inputstream from the client socket
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String PhoneLine = reader.readLine();

처리상태는 프로그램이 얼마나 실행됐냐에 달려있습니다. 다른말로하면
첫 reader.readLine() method가 리턴하면 우리는 text의 한 줄이 읽혔다는 것을 알 수 있다.
readLine() 는 한줄이 다 읽힐 때까지 block되어있기 때문입니다.

NIO 구현은 조금 다릅니다.

ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);

그래서 buffer에 충분한 데이터가 있다는것을 어떻게 알 수 있을까요? 아마 그럴 수 없을 것입니다. 해결법은 오직 buffer속 데이터를 보는 것입니다. 결과적으로 버퍼속의 데이터를 분석해야할 지 모릅니다.
이런 방식은 비효율적이고 프로그램 디자인 관점에서 복잡해질 것입니다.

ByteBuffer buffer = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buffer);
while(!bufferFull(bytesRead)) {
	bytesRead = inChannel.read(buffer);
}

여기서 bufferFull() method는 버퍼속에 얼마나 많은 데이터가 읽혔는지 추적하는 것입니다. 그리고 buffer가 가득찼는지 여부에 따라 ture or false를 리턴합니다. 즉, 버퍼가 데이터 처리를 위한 준비가 됐다면 버퍼가 가득찼을 것입니다.

만약 버퍼가 가득차면, 버퍼는 처리됩니다. 만약 가득차지않으면 어떤 데이터가 있던지간에 부분적으로 처리하는 방식이 적절하다면 부분적으로 처리할 수 있을지모릅니다. (대부분의 경우, 그렇진 않습니다.)

is-data-in-buffer-ready loop을 시각화 하면 아래와 같습니다.


요약

NIO는 다수의 Channel들을 오직 하나의 thread를 사용해서 관리할 수 있게합니다. 하지만 blocking stream을 통해 데이터를 읽을 때 보다 데이터를 처리하는 것은 좀 더 복잡할 수 있습니다.

만약 동시에 매번 소량의 데이터를 보내는 몇 천 개의connection을 관리할 필요가 있다면 예를 들어 chat server, NIO로 server를 구현하는것이 더욱 이점이 있습니다.

유사하게 만약 다른 컴퓨터와 열려있는 많은 연결을 유지할 필요가 있다면 외부 연결들을 하나의 스레드로 관리하는 것이 더욱 이점이 있습니다.

하나의 스레드, 다수의 연결 디자인은 아래와 같은 diagram으로 그릴 수 있습니다.

반대로 만약 높은 bandwidth를 가진 적은 양의 연결을 가진다면 (한번에 많은 데이터를 보내는 것) 아마 Classic IO Server구현이 더 적절할 지모릅니다. 아래는 Classic IO server Diagram입니다.



출처

profile
배운 것을 기록합니다.

0개의 댓글