개발자로서 Input/Output data를 처리하는 것은 공통적인 Task 중의 하나이다.
Java에선 이러한 Input/Output 처리를 위해 I/O package를 지원하고 있지만, JVM 위에서 동작하는 특성상 직접 메모리를 관리하여 시스템 자원을 이용하거나 OS level단의 API를 direct 호출하지 않기 때문에 C, C++ 언어와 비교했을 때 가장 차이나는 부분이다.
이로인해 Java 4부터 New I/O라는 뜻의 NIO pacakag가 포함되었고, Java IO와 NIO의 일관성없는 클래스를 바로 잡고 비동기 channel의 성능을 강화시킨 NIO2 package가 Java 7에서 기존 NIO 하위 package에 포함되게 되었다
구분 | IO | NIO |
---|---|---|
입출력 방식 | Stream | Channel |
버퍼 방식 | Non Buffer | Buffer |
비동기 방식 | 지원 X | 지원 O |
블로킹/넌블로킹 방식 | 블로킹만 지원 | 블로킹/넌블로킹 모두 지원 |
1. Stream
Stream이란 배열이나 문자열 같은 데이터 컬렉션을 말하며, 자료를 입출력하기 위하여 사용하는 것이다.
프로그램과 입출력 장치 사이에서 입출력 자료들을 중계하는 역할을 담당
한번에 1 byte씩 data를 전달하며 입력 스트림과 출력 스트림이 구분되어 있다.
데이터를 읽기 위해 Input Stream을 생성하고, 데이터를 출력하기 위해 Output Stream을 생성해야 한다.
2. Blocking mode
read(), write() 를 수행했을 때 읽거나, 쓸 데이터가 있을 때까지 blocking 된다.
IO Thread가 블로킹되면 다른 일을 할 수가 없고, blocking을 빠져나오기 위해 interrupt 할 수 없으며 유일한 방법은 스트림을 닫는 것이다.
ex) IO blocking example
InputStream inputstream = new FileInputStream("input-text.txt");
int data = inputstream.read(); // Blocking!
while(data != -1) {
doSomethingWithData(data);
data = inputstream.read();
}
inputstream.close();
1. Buffer
chunks of data를 전달하는 임시 저장 장치이며, 기본적으로 입출력 전송 속도 차이에 대한 성능을 보완하기 위해 사용한다. 입력 속도에 비해 출력 속도가 느린 경우 효율성을 위해 데이터를 임시 저장하는 공간을 말한다.
2. Channel
Stream과 달리 읽기 쓰기가 동시에 가능한 입출력 클래스이며,
데이터를 주고 받기 위해서 Thread와 데이터가 들어간 버퍼사이의 일종의 터널을 만들어주는 역할을한다.
또한, 네이티브 IO, Scatter/Gatter 구현으로 효율적 IO처리를 수행한다. (모아서 처리 --> system call 줄이기)
3. Non-blocking mode
입출력 작업 시에 Thread가 블로킹되지 않는 특성이다.
NIO의 넌블로킹은 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하므로 작업 스레드가 블로킹되지 않는다.
<-> NIO에서 블로킹은, IO와 달리 Thread를 interrupt 함으로써 빠져나올 수 있다.
4. CharsetDecoder
읽을 수 있는 문자와 raw bytes mapping (encoder, decorder api)
5. Selector
NIO 논블로킹을 위한 핵심 객체
SelectableChannel에서 멀티플렉싱을 활성화하고 I/O 준비가 된 모든 채널에 대한 액세스를 제공.
Selector에 여러개의 channel을 등록해놓고, select를 수행하면, input/write 가능한 channel을 돌려준다.
즉, 하나의 thread가 여러개의 input channel을 모니터링 하는것이 가능하다.
[Send]
Selector(Acceptor Single Thread Queue)를 생성하고 Selector에 Channel(ServerSocket)들을 등록(Write)한다.이과정에서 Selector는 단일 스레드로 멀티스레드처럼 처리가 가능하는데 이동작이 multiplexing이라 할 수 있다.
Channel 들은 처리할 이벤트를 양방향 Buffer에 등록(Write)한다.
Buffer는 해당 이벤트(notify)를 연관된 비즈니스 로직 or 연관된 시스템에게 보낸다.
[Receive]
비즈니스 로직 or 연관된 시스템이 처리한 데이터를 양방향 Buffer로 받는다.
이때 버퍼(가상 주소)를 사용하여 물리 메모리에 커널 영역의 가상 주소와 매핑시켜 커널 영역에서 유저 영역으로 데이터를 복사하지 않고 바로 참조한다.
--> zero copy!!
Netty의 Channel 은 양방향 Buffer로부터 처리된 이벤트를 등록(Write)한다.
클라이언트에서 요청이 오면 Selector는 연관된 Channel을 찾아준다. (SelectionKey로 관리한다.)
찾은 Channel에서는 처리된 이벤트를 클라이언트에게 회신한다.
신규 클라이언트에서 요청이 오면 연관된 connection인 socket Channel을 Selector에 등록하여 비동기 Non-blocking 하게 데이터를 요청한다.
public static void main(String[] args) {
Path path = Paths.get("file.txt");
// 채널 객체를 파일 읽기 모드로 생성.
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
// 1024 바이트 크기를 가진 Buffer 객체 생성
ByteBuffer buffer = ByteBuffer.allocate(1024);
ch.read(buffer);
buffer.flip();
Charset charset = Charset.defaultCharset();
String inputData = charset.decode(buffer).toString();
buffer.clear();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Path path = Paths.get("file.txt");
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
String data = "우가1999";
Charset charset = Charset.defaultCharset();
ByteBuffer buffer = charset.encode(data);
ch.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
NIO는 다수의 Channel들을 오직 하나의 thread를 사용해서 관리할 수 있으므로 scalability가 좋지만, non-blocking 특성으로 데이터를 처리하는 것은 좀 더 복잡할 수 있다.
만약 동시에 매번 소량의 데이터를 보내는 몇 천 개의connection을 관리할 필요가 있다면 (ex: chat server), NIO로 server를 구현하는것이 더욱 이점이 있다. 유사하게 만약 다른 컴퓨터와 열려있는 많은 연결을 유지할 필요가 있다면 외부 연결들을 하나의 스레드로 관리하는 것이 더욱 이점이 있다.
하지만, 반대로 연결 클라이언트 수가 적고 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있는 경우 IO로 서버를 구현하는 것이 좋다.
NIO는 버퍼 할당 크기도 문제되고, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 받은 즉시 처리하는 IO보다 복잡하다.
ref