Java를 공부하다보면, IO는 들어봤어도 NIO는 처음들어볼 수도 있다.
이번에는 Java의 새로운 IO 메커니즘인 NIO에 대해서 알아보자.
- NIO (New Input/Ouput)
- Java 4에서 새롭게 등장한 입출력 패키지 / 메커니즘
- Java 4에서 처음 등장했지만, Java 7에서 조금더 개선되었다. (버전 2라고 불린다.)
Java를 사용해서 코딩 테스트, 네트워크 프로그래밍을 한 경험이 있다면, 아래와 같은 기본 입출력 스트림 계보는 모두 접했을 것이다.
이는 Java에서 입출력을 구현한다고 하면, 흔히 사용되는 입출력 클래스들로 통칭 IO Stream 이라고 부른다.
하지만 Java 4 부터 새로운 입출력 패키지가 등장한다. 바로 java.nio 이다.
nio의 뜻은 New Input Output 이라는 뜻으로, 스트림 기반의 입출력 방식과는 다른 방식을 사용하는 입출력이다.
이외에도 기존에 자바에서 파일을 다룰 때 사용하는 방식 또한 변경되었다.
IO는 InputStream, OutputStream 기반으로 입출력을 수행하는 반면에
NIO는 Stream이 아닌, Channel 과 Buffer 기반으로 입출력을 수행한다.
기존의 자바 IO에서 파일을 다룰 때는 File 클래스로 다루었다.
하지만 NIO에서는 Path로 다룬다.
// IO
File file = new File(String fileName);
// NIO
Path path = Paths.get(String first, String... more)
Path path = Paths.get(URI uri);
NIO에서의 Channel 은 IO에서 InputStream, OutputStream 과 동일한 역할을 수행한다고 생각하면 된다.
FileInputStream,FileOutputStream 처럼 FileChannel 이 존재한다.
이외에도 다양한 Channel이 존재한다.
아래 사진을 보면, 파일(또는 프로세스)와 연결된 채널을 생성하여, 하나의 채널로 입출력을 수행하는 것을 볼 수 있다.
Buffer는 BufferedReader에서 입출력 성능을 향상하기 위해 사용되는 버퍼와 동일한 의미이고 일반 IO보다 성능이 좋다.
(NIO에서는 입출력을 수행할 때 무조건 버퍼가 필요하다고 보면된다.)
Buffer에는 버퍼의 위치에 따라 두 가지 종류가 존재한다.
또한 어떤 타입을 지원하는지에 따라서 타입 별로 구분할 수도 있다.
스트림 기반의 기존 IO는 블로킹 방식으로 동작한다.
어떤 스레드에서 InputStream의 read() 를 호출하여 데이터를 읽어들일 때,
데이터가 준비될 때까지 무한정 대기하여 블록되기 때문이다.
하지만 NIO는 비동기 Non블로킹 방식으로 IO 작업을 수행할 수 있다.
AsychronousFileChannel 처럼,
Asynchronous 접두사가 붙은 클래스를 사용하면, 별도의 스레드풀을 사용해서 비동기적으로 IO를 수행할 수 있다.
이벤트 루프 방식 기반의 IO를 지원하기 위해 설계된 클래스이다.
운영체제의 SystemCall 중에서 select() 라는 시스템 콜이 있다.
이 시스템 콜은 여러 소켓들 중에서 readable 하거나 writable한 소켓들을 알려주는데
Java의 NIO에서는 이와 비슷한 동작을 수행하는 Selector가 존재한다.
select() 시스템 콜은 OS 레벨에서 동작하는 반면, Selector 는 이와 비슷한 동작을 애플리케이션 레벨에서 지원하기 위해서 만들어졌다.
이외에도, IO의 ServerSocket은 NIO에서 ServerSocketChannel과 매핑되고
IO의 Socket은 NIO의 SocketChannel 과 대응된다.
기존 역할과 계층의 구성은 비슷하지만, 입출력을 수행하는 방식이 약간 다르다.
IO는 입력과 출력이 다른 Stream 에서 이루어지지만, NIO는 입력과 출력이 하나의 Channel에서 수행되기 때문이다.
사진으로 표현하자면 아래와 같다.
| IO | NIO |
|---|---|
|
|
멀티플렉싱 I/O
간단하게만 말하면 멀티플렉싱 I/O가 가능해진다.
여기서 말하는 멀티 플렉싱이란,
하나의 스레드가 하나의 입력 또는 출력을 수행하는 기존의 방식과는 달리,
하나의 스레드로 여러 입/출력을 동시에 수행하는 작업을 말한다.
(멀티 플렉싱 IO는 스레드풀 방식의 IO 모델이 가진 한계를 극복해준다.)
그러면 왜 이러한 차이가 발생하고, 멀티플렉싱 IO가 더 좋은지 알아보자.
기존의 IO Stream을 사용하여 입출력을 수행하면, 입/출력 작업을 수행할 때 블로킹되므로 입출력 작업을 수행하는 스레드는 다른 작업을 수행하지 못한다.
그래서 여러 입출력을 동시에 수행하기 위해서는 필연적으로 스레드 풀 방식의 입출력 모델을 채택하여 여러 스레드가 동시에 입출력을 수행할 수 있도록 해야 한다.
스레드 풀 방식의 입출력 모델이 가지는 한계점은 바로 스레드 숫자에 있다.
이 방식으로 높은 처리량을 구현하려면, 스레드 풀의 사이즈를 늘려야 한다. 하지만 스레드의 수가 많아지면, 높은 컨텍스트 스위칭의 비용이 발생하므로, 단순히 스레드 수를 늘리는 것은 좋지 않은 방법이다.
그래서,
하지만 NIO 에서는 Selector를 도입하여, 하나의 스레드가 여러 채널를 관리하여, 여러 채널에 대해서 입력/출력을 동시에 수행할 수 있다.
즉, 하나의 스레드만으로도 여러 입출력을 동시에 수행할 수 있다는 뜻이다.
Selector 는 각 Channel에 Read 또는 Write 이벤트가 발생하면, 이 이벤트에 맞는 핸들러와 연결해주는 중재자 역할을 해준다고 생각하면 된다.
(JavaScript의 이벤트 루프처럼 동작한다고 생각하면 이해가 편하다.)
이렇게, 멀티 플렉싱 IO 모델은 스레드풀 방식의 IO 모델이 가진 한계를 극복하게 해주어, 동시에 처리할 수 있는 작업을 월등하게 높일 수 있다.
실제로 Tomcat 과 양대산맥이라고 불리는 Netty 서버는 이벤트 루프 방식(멀티플렉싱 IO)을 채택하여 높은 처리량을 보여준다.
NIO의 Selector 와 멀티플렉싱 IO 방식에 대해서는 다음 글에서 조금 더 자세히 다뤄보고자 한다.
새로운 방식의 NIO는, Stream 기반의 IO 방식과는 완전히 새로운 방법을 보여준다.
Path, Channel, Buffer, Selector 등의 클래스를 도입하여, 스레드풀 방식의 IO 모델이 가진 한계를 더 적은 자원을 사용하여 극복하게 해준다.
이는 Netty 뿐만 아니라 Spring WebFlux 에서도 사용되고, 적은 자원을 사용하여 효율적으로 처리하기 위해 사용된다.
프로젝트의 처리량과 부하에 따라서 기술 스택의 선택지가 갈릴 수 있는데,
보편적으로 Spring MVC / Tomcat 과 Spring WebFlux / Netty 선택지로 많이 구분된다.
그러나 Silver Bullet인 기술은 존재하지 않으므로 서비스의 상황과 애플리케이션의 상태에 맞는 기술을 선택하는 것이 중요하겠다.