feat. 친구의 팩폭
일단 자바는 C/C++과 달리 직접 메모리를 관리하고 OS 레벨의 시스템 콜을 직접 사용하기는 어렵다. JNI를 사용하는것은 여기서는 배제하도록 하자. 자바는 JVM 위에서 동작하므로 주로 C/C++에 비하면 자바는 느리다고 인식되고 실제로도 그렇다.
(여담: 친구들과 자바는 느리다 C++이 빠르다 어쩌구저쩌구 토론할 때.. 어느 한 친구가.. '제임스 고슬링이 만든 Java서버 우리가 만든 C++서버 뭐가 더 빠를까?' 우리 모두는 침묵하였다... 또.르..르...)
자바가 특별히 성능이 좋지 않은 부분은 IO다. IO 성능 문제를 개선하는 것이 바로 java.nio 패키지이다.
자바 4부터 새로운 입출력(NIO: New Input/Output)이라는 뜻에서 java.nio 패키지가 포함되었는데, 자바 7부터 자바 IO와 자바 NIO 사이의 일관성 없는 클래스 설계를 바로 잡고, 비동기 채널 등의 네트워크 지원을 대폭 강화한 NIO.2 API가 추가되었다. NIO.2는 java.nio2 패키지로 제공되지 않고 기존 java.nio의 하위 패키지 java.nio.channels, java.nio.charset, java.nio.file에 통합되어 있다.
스트림 VS 채널
IO는 스트림 기반이다. 스트림은 입력 스트림과 출력 스트림으로 구분되어 있기 때문에 데이터를 읽기 위해서는 입력 스트림을 생성해야 하고, 데이터를 출력하기 위해서는 출력 스트림을 생성해야 한다. NIO는 채널 기반. 채널은 스트림과 달리 양방향으로 입력과 출력이 가능하다. 그렇기 때문에 입력과 출력을 위한 별도의 채널을 만들 필요가 없다.
넌버퍼 VS 버퍼
IO에서는 출력 스트림이 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다. 이것보다는 버퍼를 사용해서 복수 개의 바이트를 한꺼번에 입력받고 출력하는 것이 빠른 성능을 낸다. 그래서 IO는 버퍼를 제공해 주는 보조 스트림인 BufferedInputStream, BufferedOutputStream을 연결해서 사용하기도 한다. NIO는 기본적으로 버퍼를 사용해서 입출력을 하므로 IO보다는 성능이 좋다. 채널은 버퍼에 저장된 데이터를 출력하고, 입력된 데이터를 버퍼에 저장한다.
또한, IO는 스트림에서 읽은 데이터를 즉시 처리하기 때문에 입력된 전체 데이터를 별도로 저장하지 않으면, 입력된 데이터의 위치를 자유롭게 이용할 수 없다. NIO는 읽은 데이터를 무조건 버퍼에 저장하기 때문에 버퍼 내에서 데이터의 위치를 이동해 가면서 필요한 부분만 읽고 쓸 수 있다.
블로킹 VS 넌블로킹
IO는 블로킹(blocking)이 된다. 입력 스트림의 read() 출력 스트림의 write() 메소드를 호출하면 블로킹 된다. IO 스레드가 블로킹 되면 다른 일을 할 수 없고 블로킹을 빠져나오기 위해 인터럽트도 할 수 없고 블로킹을 빠져나오는 방법은 스트림을 닫는 것이다.
NIO는 블로킹과 넌블로킹 특징을 모두 가지고 있다. IO 블로킹과의 차이점은 NIO 블로킹은 스레드를 인터럽트 함으로써 빠져나올 수가 있다는 것이다. NIO의 넌블로킹은 입출력 작업 준비가 완료된 채널만 선택해서 작업 스레드가 처리하기 때문에 작업 스레드가 블로킹 되지 않는다. NIO 넌블로킹의 핵심 객체는 멀티플렉서인 Selector이다. 셀렉터는 복수 개의 채널 중에서 이벤트가 준비 완료된 채널을 선택하는 방법을 제공해준다.
소켓을 통해 non-blocking read를 할 수 있도록 지원하는 connection.
읽기, 쓰기 하나씩 쓸 수 있는 스트림은 단방향식, 채널은 읽기 쓰기 둘 다 가능한 양방향식 입출력 클래스이며 네이티브 IO , Scatter/Gather 구현으로 효율적인 IO 처리 (시스템 콜 수 줄이기, 모아서 처리하기)
커널에 의해 관리되는 시스템 메모리를 직접 사용할 수 있는 채널에 의해 직접 read 되거나 write 될 수 있는 배열과 같은 객체.
네트워크 프로그래밍의 효율을 높이기 위한 것
클라이언트 하나당 쓰레드 하나를 생성해서 처리하기 때문에 쓰레드가 많이 생성될수록 급격한 성능 저하를 가졌던 단점을 개선하는 Reactor 패턴의 구현체
Selector는 어느 channel set이 IO event를 가지고 있는지를 알려준다. Selector.select() 는 I/O 이벤트가 발생한 채널 set을 return 한다. return 할 channel이 없다면 계속 block 된다. 이 block된 것을 바로 return 시켜주는 것이 Selector.wakeup()이다.
Selector.selectedKeys()는 Selection Key를 return 해 준다. Reactor는 이 Selection Key를 보고 어떤 handler로 넘겨줄지를 결정한다.
Selector와 Channel 간의 관계를 표현해주는 객체이다. Selector가 제공한 Selection Key를 이용해 Reactor는 채널에서 발생하는 I/O 이벤트로 수행할 작업을 선택할 수 있다. ServerSocketChannel 에 selector를 등록하면 key를 준다. 이 key가 SelectionKey 이다.
캐릭터셋을 나타낸다. 바이트 데이터와 문자 데이터를 인코딩 디코딩할때 사용된다.
NIO는 다수의 연결이나 파일들을 넌블로킹이나 비동기 처리할 수 있어서 많은 스레드 생성을 피하고 스레드를 효과적으로 재사용한다는 장점이 있다. 그래서 NIO는 연결 수가 많고 하나의 입출력 처리 작업이 오래 걸리지 않는 경우에 사용하는 것이 좋을 것이다. 스레드에서 입출력 처리가 오래 걸린다면 대기하는 작업의 수가 늘어나게 되므로 장점이 사라진다.
많은 데이터 처리의 경우 IO가 좋을 수 있다. NIO는 버퍼 할당 크기가 문제가 되고, 모든 입출력 작업에 버퍼를 무조건 사용해야 하므로 즉시 처리 하는 IO보다 성능 저하가 있을 수 있다. 연결 클라이언트 수가 적고 전송되는 데이터가 대용량이면서 순차적으로 처리될 필요성이 있는 경우 IO로 구현하는 것이 좋은 선택일 수 있다.
- https://altongmon.tistory.com/284
- https://marshallslee.tistory.com/entry/자바-NIO-들어가기
- https://sarc.io/index.php/miscellaneous/678-java-nio
- https://javacan.tistory.com/entry/87
- https://javacan.tistory.com/entry/73
- https://palpit.tistory.com/640
- https://segmentfault.com/a/1190000006824196
- https://jongmin92.github.io/2019/03/03/Java/java-nio/