일부러 낚시 냄새가 독하게 풍기는 제목을 지어봤다.

Java NIO는 New IO의 줄임말인데, Non-blocking IO 의 줄임말이라고 알고 있는 개발자도 많은 것 같다.(나도 그랬다..)

그만큼 NIO는 Non-blocking이라는 마케팅이 꽤나 열심이었고, 또 그게 잘 먹혔기 때문인지, File I/O를 사용할 때마저 기존의 IO 방식 대신 NIO를 쓰면 Non-blocking이라서 좋다는 글도 많다. 그냥 그런가보다.. 했는데 알고보니 그렇지 않더라는..

IO : NIO = Stream : Channel

옛날에 적성검사나 카투사 시험에서 다음과 같은 유형의 문제와 자주 맞닥뜨렸던 기억이 있다.

1
고모 : 이모 = 아빠 : ?
1
아빠 : 형 = ? : 누나

뭐랄까.. 가족계보를 선형대수로 알싸하게 풀어낸, 실로 참신하고 현란하기 그지 없는 문제들이다.

IO와 NIO의 관계도 위와 같은 현란한 비례식으로 생각해보면 머리에 조금이라도 더 남을 수 있을 것만 같다. 특히나 IO : NIO = Blocking : Non-blocking 이라는, 조금 섣부른 넘겨집기에서 나온 비례식은 아래와 같이 바꾸는 것이 좋을 것 같다.

IO : NIO = Stream : Channel

잔말이 많았는데, IO와 NIO가 뭐가 다른지 알려면 StreamChannel의 차이점을 먼저 알아야 한다.

Stream vs Channel

기존의 Stream은 읽을 때와 쓸 때 InputStreamOutputStream으로 구분해서 사용했다. Stream을 통해 흘러다니는 데이터는 기본적으로는 byte 또는 byte[]이고, 읽고 쓰는 작업을 지시한 후에는 그 작업이 끝나야 return 되는 bloking 방식이다.

Channel은 데이터가 흘러다니는 통로라는 점에서 Stream과 역할은 비슷하지만 동작 방식이 다르다.

단방향인 Stream과는 달리 Channel은 양방향이라서 intput/output을 구분하지 않고 그냥 ByteChannel, FileChannel를 만들어서 읽을 수도 있고, 쓸 수도 있다.

Channel은 언제나 Buffer를 통해서만 데이터를 읽거나 쓸 수 있다. Channel에서 데이터를 읽으면 Buffer에 담아야만 어떤 처리를 할 수 있고, Channel에 데이터를 쓰려면 먼저 Buffer에 담고, Buffer에 담긴 데이터를 Channel에 써야 한다.

Channel은 Non-blocking 방식도 가능하다. 다시 말하지만, Channel을 사용하는 I/O는 언제나 Non-blocking 방식으로 동작하는 것이 아니라, Non-blocking 방식도 가능하다는 것이다.

정리하면 아래와 같다.

Stream Channel
one-way(InputStream or OutputStream) two-way(Channel)
구현체에 따라 primitive부터 object까지 가능하나, 기본적으로는 byte 또는 byte[] Buffer
Blocking Non-bloking도 가능 (언제나 Non-blocking인 것이 아니라)

Files.new~~()는 모두 blocking이다.

java.nio.Files는 NIO 중에서 File I/O를 담당하는데, 결론부터 말하면,

파일을 읽는데 사용되는 Files.newBufferedReader(), Files.newInputStream() 등은 모두 blocking이다.

파일을 쓰는데 사용되는 Files.newBufferedWriter(), Files.newOutputStream() 등도 모두 blocking이다.

진짜?

위 메서드들은 결국 Files.newByteChannel()을 통해 생성되는 SeekableByteChannel을 바탕으로 데이터를 읽거나 쓰게 되는데, 이 SeekableByteChannelReadableByteChannelWritableByteChannel을 구현하고 있다.

그런데 ReadableByteChannelWritableByteChannel은 모두 blocking 모드로만 동작하는 Channel이다.

근거는? Java API에 다음과 같이 적혀 있다.

https://docs.oracle.com/javase/8/docs/api/java/nio/channels/ReadableByteChannel.html

Interface ReadableByteChannel

Only one read operation upon a readable channel may be in progress at any given time. If one thread initiates a read operation upon a channel then any other thread that attempts to initiate another read operation will block until the first operation is complete. Whether or not other kinds of I/O operations may proceed concurrently with a read operation depends upon the type of the channel.

https://docs.oracle.com/javase/8/docs/api/java/nio/channels/WritableByteChannel.html

Interface WritableByteChannel

A channel that can write bytes.
Only one write operation upon a writable channel may be in progress at any given time. If one thread initiates a write operation upon a channel then any other thread that attempts to initiate another write operation will block until the first operation is complete. Whether or not other kinds of I/O operations may proceed concurrently with a write operation depends upon the type of the channel.

결국 NIO 중에서 File I/O에 관련된 것들은 아쉽지만 모두 blocking인 것이다.

Files.new~~() 외에 Files.lines(), Files.readAllLines(), Files.readAllBytes(), Files.write()도 위에 설명한 것과 마찬가지 이유로 모두 blocking이다.

어차피 blocking인데 File I/O에 뭐하러 NIO를 써?

성능

File I/O에 사용되는 Channel이 blocking 모드로 동작하기는 하지만, 데이터를 Buffer를 통해 이동시키므로써 기존의 Stream I/O에서 병목을 유발하는 몇가지 레이어를 건너뛸 수 있다고 한다.(https://docs.oracle.com/javase/tutorial/essential/io/file.html)

더 구체적으로는 Buffer를 사용하면 DMA를 활용할 수 있다는 건데, 여기에 한글로 아주 설명이 잘 되어있다.

그리고 NIO를 쓰는 게 낫다는 자료는 인터넷에서 쉽게 찾을 수 있다.

하지만 성능 관련 내용이 이렇게 명백하게 갈릴리가..
참고로 다음과 같은 반론도 있다.

기능

기존의 java.io.File에는 기능적으로 다음과 같은 단점이 있다고 하니, 당연하지만 NIO를 쓸 수 있다면 쓰는 것이 좋다.(https://docs.oracle.com/javase/tutorial/essential/io/legacy.html#mapping)

  • Many methods didn’t throw exceptions when they failed, so it was impossible to obtain a useful error message. For example, if a file deletion failed, the program would receive a “delete fail” but wouldn’t know if it was because the file didn’t exist, the user didn’t have permissions, or there was some other problem.
  • The rename method didn’t work consistently across platforms.
  • There was no real support for symbolic links.
  • More support for metadata was desired, such as file permissions, file owner, and other security attributes.
  • Accessing file metadata was inefficient.
  • Many of the File methods didn’t scale. Requesting a large directory listing over a server could result in a hang. Large directories could also cause memory resource problems, resulting in a denial of service.
  • It was not possible to write reliable code that could recursively walk a file tree and respond appropriately if there were circular symbolic links.

Non-blocking은 그럼 구라?

아니다. NIO가 생각만큼 Non-blocking하지 않다는 것이지, Non-blocking이 구라라는 소리는 아니다.

Non File I/O

File I/O가 아닌 것들은 Non-blocking 모드로 동작하는 것들도 많다.
SelectableChannel을 상속받거나 구현한 Channel은 Non-blocking 모드로 동작할 수 있다. 예를 들어, DatagramChannel, Pipe.SourceChannel, SocketChannelReadableByteChannelSelectableChannel을 함께 구현하고 있어서 Non-blocking 모드로 읽는 것이 가능하다.

File I/O에서는 정녕 Non-blocking은 없나?

있다. java 7 부터 도입되어 NIO2라고 불리는 NIO에는 AsynchronousFileChannel이 Non-blocking 모드로 동작한다. AsynchronousFileChannel은 별도의 글에서 다뤄본다.

정리

  • NIO는 Non-blocking IO의 줄임말이 아니다.
  • 특히, File I/O는 NIO에 포함된 java.nio.Files 클래스를 사용해도 여전히 blocking모드로 동작한다.
  • NIO에서의 File I/O가 기대와는 달리 blocking 모드로 동작한다고 해도, 기능/성능 상으로 유리한 점이 있으니 가능하다면 NIO를 쓰자(성능은 반론도 있기는 하다).

더 읽을거리


크리에이티브 커먼즈 라이선스HomoEfficio가 작성한 이 저작물은(는) 크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.