Java NIO와 IO API 모두 공부할 때, 마음속에 질문 하나가 떠오른다:
IO는 언제 사용해야하고 NIO는 언제 사용해야하는가?
이 텍스트에서 필자는 Java NIO와 IO 간에 있어 그들의 사용하는 경우와 코드 설계에 있어 어떻게 영향을 미치는지 일부 차이점들을 밝혀보고자 한다.
Main Differences Betwen Java NIO and IO
아래 테이블은 Java NIO와 IO 간의 주요 차이점들을 요약한다. 필자는 테이블 뒤 섹션에서 각각의 다른점들에 관한 세부적인 정보들을 가져올 것이다.
IO | NIO |
스트림 중심 | 버퍼 중심 |
블럭킹 IO | 비블럭킹 IO |
셀렉터 |
Stream Oriented vs. Buffer Oriented
첫번째 Java NIO와 IO 간의 가장 큰 차이점은 IO는 스트림 중심이고, NIO는 버퍼 중심이라는 것이다. 그래서 그것은 무엇을 의미할까?
스트림 중심인 Java IO는 스트림으로부터 한번에 한 바이트 이상을 읽는 것을 의미한다. 읽은 bytes로 무엇을 할지는 개발자에게 달려있다. 그것들은 어디에도 캐시되지 않는다. 뿐만 아니라, 스트림 데이터의 앞뒤로 이동할 수도 없다. 만약 스트림으로부터 읽은 데이터의 앞뒤로 이동할 필요가 있다면, 먼저 버퍼에 캐시를 할 필요가 있을 것이다.
Java NIO의 버퍼 중심의 접근은 약간 다르다. 데이터는 추후 처리된 것으로부터 버퍼에 읽어진다. 필요에 따라 버퍼의 앞뒤로 이동이 가능하다. 이는 처리하는 동안 조금 더 유연성을 가져다 준다. 하지만, 버퍼가 그것을 완전히 처리하기 위해 필요한 모든 데이터가 버퍼에 포함되는지 확인해야만 한다. 그리고 버퍼에 더 많은 데이터를 읽을 때, 아직 처리되지 않은 버퍼에 데이터를 덮어쓰지 않는지 확인해야만 한다.
Blocking vs. Non-blocking IO
Java IO의 다양한 스트림들은 블럭킹된다. 스레드가 read()를 호출할 때 읽기 위한 일부 데이터가 들어올 때까지 블럭킹되거나, write()를 호출할 때 데이터가 완전히 기록될 때까지 블럭킹됨을 의미한다. 스레드는 그 동안에 아무일도 할 수 없다.
Java NIO의 비블럭킹 모드는 스레드가 채널로부터 읽은 데이터를 요청하는 것을 가능케 하고, 현재 이용할 수 있는 것들만 가져올 수 있으며, 현재 이용할 수 있는 데이터가 아무것도 없다면 아무일도 하지 않는다. 오히려 데이터가 읽을 수 있을 때 까지 남아있는 것이 블럭되는 것 보다, 스레드는 무언가 다른 것을 할 수 있는게 낫다.
비블럭킹 기록하기도 마찬가지이다. 한 스레드는 채널에 일부 기록된 데이터를 요청할 수 있지만, 그것이 완전히 기록될 때까지 기다리지 않는다. 그 스레드는 그 동안 무언가 다른 작업을 계속 할 수 있다.
스레드들이 IO 호출들에서 블럭되지 않을 때의 대기 시간을 보내는 것은, 보통 그 동안 다른 채널들의 IO 작업을 말한다. 즉, 이제 단일 스레드는 입출력 다중 채널들의 관리가 가능하다.
Selectors
Java NIO의 셀렉터는 단일 스레드가 입력 다중 채널 모니터링을 허용한다. 셀렉터로 다중 채널들의 등록이 가능하고, 그러면 작업을 위한 입력 가능한 채널 "선택"에 있어 단일 스레드를 사용 하거나, 기록하기 위해 준비된 채널들을 선택한다. 이 셀렉터 메카니즘은 단일 스레드에서 다중 채널들의 관리를 쉽게 해준다.
How NIO and IO Influences Application Design
IO 툴킷으로 NIO나 IO중에 하나를 선택하는 것은 애플리케이션 설계의 다음과 같은 측면에 영향을 미칠지도 모른다:
- API는 NIO나 IO 클래스들을 호출.
- 데이터 처리.
- 데이터 처리에 사용되는 스레드의 수.
The API Calls
물론 API 호출들이 IO를 사용할 때보다 NIO를 사용할 때 다르게 보인다. 이것은 전혀 놀라운 사실이 아니다. 예를 들어 InputStream에서의 바이트를 위한 데이터 바이트를 단순히 읽는 것보다, 데이터가 버퍼에 맨 처음 읽히고 그곳으로부터 작업되도록 해야만 하는 것이 낫다.
The Processing of Data
데이터의 처리는 순수 NIO 설계 vs IO 설계의 사용에도 영향을 미친다.
IO 설계에서 InputStream 혹은 Reader로부터 바이트의 데이터 바이트를 읽는다. 라인 기반 텍스트 데이터를 스트림으로 처리했다고 가정해보자. 다음 예는:
Name: Anna Age: 25 Email: anna@mailserver.com Phone: 1234567890
텍스트 라인들의 스트림은 아래와 같이 처리될 수 있다:
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() 메소드가 반환할 때 텍스트의 전체 라인이 읽어졌다는 것을 물론 알고 있다. 그 이유로, readLine()은 라인을 모두 읽을 때 까지 블럭한다. 이 라인은 이름을 포함하고 있다는 것도 알고 있다. 마찬가지로, 두번째 readLine() 호출이 반환할 때, 이 라인은 age등을 포함하고 있다는 것을 알고 있다.
보다시피, 새로운 데이터를 읽을 수 있는 경우에만 프로그램이 진행하고 각 단계에 해당하는 데이터가 무엇인지 알고 있다. 실행중인 스레드가 코드의 데이터 조각을 확실히 읽고 지나갔다면, 스레드는 데이터의 뒤로(주로 하지 않음) 돌아가지 않는다. 이 원칙은 다음 다이어그램서도 보여준다:
Java IO: 블럭킹 스트림으로부터의 데이터 읽기. |
NIO 구현은 다르게 보일 것이다. 다음 간략한 예제이다:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer);
채널로부터 (바이트버퍼)에 바이트를 읽은 두번째 라인에 주목하기 바란다. 메소드가 호출하여 반환할 때, 버퍼 안에 필요한 모든 데이터가 있는지 여부는 알 수 없다. 알다시피, 버퍼는 일부 바이트만을 포함한다. 이 처리가 조금은 어렵게 만든다.
첫 read(buffer) 호출 후에, 버퍼에 읽어진 것들 모두가 라인의 절반이라고 가정해보자. "Name: An" 이라고 예를 들어보자. 저 데이터를 처리할 수 있을까? 그렇지 않다. 모든 데이터의 처리가 타당하다 생각하기 앞서, 버퍼에 적어도 데이터의 라인 전체가 있을 때 까지 기다려야만 한다.
그래서 버퍼가 처리하기에 타당한 충분한 데이터를 포함하는지 어떻게 알수 있을까? 물론, 알 수 없다. 알아낼 수 있는 유일한 방법은 버퍼의 데이터를 들여다 보는 것 뿐이다. 그 결과, 모든 데이터가 버퍼에 있는지 알기 이전에 여러번 버퍼 데이터를 검사해야 할 지도 모른다. 두가지 모두 비효율적이고, 프로그램 설계 관점에서 엉망이 될 수 있다. 예를 들어:
ByteBuffer buffer = ByteBuffer.allocate(48); int bytesRead = inChannel.read(buffer); while(! bufferFull(bytesRead) ) { bytesRead = inChannel.read(buffer); }
bufferFull() 메소드는 버퍼에 읽어진 데이터가 얼만큼인지 추적을 유지할 수 있고, 버퍼가 가득 찼는지 여부에 따라 true나 false를 반환한다. 바꿔 말하면, 버퍼가 작업을 위한 준비가 되었다면, 가득 찼다는 것이 고려된다.
bufferFull() 메소드는 버퍼를 검색하지만 bufferFull() 메소드가 호출되기 전과 같은 상태로 버퍼를 남겨둬야만 한다. 만약 그렇지 않다면, 버퍼에 읽은 다음 데이터는 올바른 위치에 읽어지지 않을 수도 있다. 이는 불가능하진 않지만, 조심해야 할 또다른 문제이다.
만약 버퍼가 가득 찼다면, 버퍼는 처리될 수 있다. 만약 가득차지 않고, 특별한 경우에서 타당하다면 데이터를 부분적으로 처리할 수도 있다. 많은 경우에 그렇지 않다.
다음 그림은 is-data-in-buffer-ready 반복을 보여준다:
Java NIO: 버퍼에 모든 데이터가 필요해 질 때까지 채널로부터 데이터 읽기. |
Summary
NIO는 단일 스레드(혹은 적은)만 사용하여 다중 채널들(네트워크 연결이나 파일들)의 관리를 허용하지만, 블럭킹 스트림으로부터 데이터를 읽을 때 보다 데이터를 파싱하는데 비용이 다소 복잡해 진다는 것이다.
만약 채팅서버와 같이 동시에 데이터를 조금씩 전송하는 수천개의 연결들을 관리할 필요가 있다면, NIO로 구현한 서버가 장점이 있다. 마찬가지로, P2P 네트워크와 같이 다른 컴퓨터들의 열린 연결들의 많은 유지가 필요할 경우, 아웃바운드 연결들의 모든 관리를 위해 단일 스레드를 사용하는 것이 장점이 될 수도 있다. 다음 다이어그램은 하나의 스레드, 다중 연결들의 설계이다:
Java NIO: 단일 스레드의 다중 연결 관리. |
만약 매우 높은 대역폭에서 더 적은 연결들을 갖고 있는 경우 한꺼번에 많은 데이터를 전송하게 되면, 고전 IO 서버의 구현이 최적일수도 있다. 다음 다이어그램은 고전 IO 서버 설계이다:
Java IO: 고전 IO 서버 설계 - 한 스레드당 하나의 연결. |
'Programming > Java NIO' 카테고리의 다른 글
Files (0) | 2015.09.15 |
---|---|
Path (0) | 2015.09.15 |
Pipe (0) | 2015.09.15 |
DatagramChannel (0) | 2015.09.11 |
ServerSocketChannel (0) | 2015.09.10 |