SSARTEL-10th / JPTS_bookstudy

"개발자가 반드시 알아야 할 자바 성능 튜닝 이야기" 완전 정복
7 stars 0 forks source link

NIO 그게 대체 뭔데..? #10

Open daminzzi opened 1 year ago

daminzzi commented 1 year ago

👍 문제

책에서는 간략히 여러가지 IO와 IO의 단점을 보완하기 위한 NIO를 소개하였다. 하지만 그 과정에서 설명이 부족하다고 느껴지는 부분이 있어 문제를 제시하고자 한다. NIO의 어떤 특성으로 인해서 IO의 단점을 보완할 수 있는지, 또한 NIO를 사용해야하는 use-case에 대해서 알아보자.

✈️ 선정 배경

알고리즘만 풀던 나에게는 낯선 NIO를 좀 더 자세히 알아볼 수 있으면 좋겠다는 생각에 이 문제를 선정하게 되었다.

📺 관련 챕터 및 레퍼런스

ch9. IO에서 발생하는 병목 현상

🐳 비고

kgh2120 commented 1 year ago

들어가며

Java의 입출력을 담당하는 패키지로 알고리즘 등에서 이용해봤을 듯 한 java.io가 있다. 하지만 java.io의 단점을 극복하기 위해서 새로 등장한 패키지가 있으니, 이것이 java.nio라고 한다. nio란 New IO라는 뜻이라는데, 대체 기존 I/O의 어떤 점을 변경하고 싶어서 새로운 패키지를 만들었을까?

Java NIO

IO와의 차이

Java NIO는 Java에서의 IO의 단점을 극복하기 위해서 Java 4버전에서부터 추가되었고, Java 7과 8에서 새로운 기능이 추가되면서, 상당한 성능의 발전을 보였다. 기존의 Java IO와의 차이점으로는 크게 2가지를 고를 수 있는데, 이는 사용 편의성성능이다. (Java 4에서 추가된 것과 7,8에서 추가된 기능을 찾으려고 하는데, 기존에 있던 패키지에 통합되어서 알기힘들다ㅠㅠ)

사용 편의성

기존의 io패키지에서 파일을 이용할 때에는 File이라는 클래스를 이용해서 굉장히 많은 역할을 했다. File이 경로, 디렉토리, 파일의 역할을 전부 했었고, File 클래스에 담긴 메서드 역시 굉장히 많았다. 하지만 nio패키지에선 경로를 Path, 디렉토리와 파일들에 대한 작업을 Files에서 진행한다.

또한 예외 클래스 역시 다양해져서 기존의 IOException의 에러 메세지를 통해서 에러 처리를 다르게 해야 했던 것과 달리 nio에서는 19개의 Exception class가 추가되었다고 한다.

java io와 nio의 사용 방법의 차이는 아래와 같다. nio에서는 Path와 유틸 클래스인 Files를 이용해 더욱 명확하게 작업을 할 수 있다.

      // 파일 생성
        File file = new File("/hello.txt");
        file.createNewFile();

        Path path = Paths.get("/hello.txt");
        Files.createFile(path);

        // 디렉토리 생성
        File dir = new File("/assets");
        dir.mkdir();

        Path dirPath = Paths.get("/assets");
        Files.createDirectory(dirPath);

또한 파일을 입출력할 때 버퍼를 이용하는 것 역시 쉽다. (Files를 이용해서 생성하는 BufferedReader와 같은 객체는 이후 설명할 Channel 을 이용해서 만들어지기 때문에, 내부적으로 Buffer를 이용해서 최적화가 이루어진다.)

// 버퍼 리더 생성
        File bufFile = new File("/hello.txt");
        BufferedReader br1 = new BufferedReader(new FileReader(bufFile));

        Path bufPath = Paths.get("/hello.txt");
        BufferedReader br2 = Files.newBufferedReader(bufPath);

성능

io와의 성능의 차이를 보이는 주된 요소는 3가지로 꼽을 수 있다. Buffer기반의 작동, Direct Byte Buffer를 통한 Native 수준의 I/O 제공, 논 블록킹 방식의 제공이다.

Buffer 기반 작동

NIO에서는 기존 IO의 Stream 방법과 달리 Buffer기반으로 작동한다. 기존의 자바 IO에서 그 성능을 향상시키기 위해서 BufferedReader와 같은 보조스트림을 이용했던 것을 기억하면 Buffer 기반이 성능이 좋다는 것을 알 수 있다.

NIO에서 기본적으로 이용하는 통신 객체는 Channel이 있다. Channel은 내부적으로 Buffer로 이루어져있어서, Stream보다 성능이 뛰어나다. 또한 Stream과 달리 양방향 통신이 가능하여 파라미터를 통해 그 방향을 설정할 수 있다.

NIO의 패키지인 files에서 생성되는 객체들 ex) .newBufferedReader() , .newInputStream() 은 Channel로 생성되기 때문에 동일한 이름 같이 보여도 성능이 뛰어날 수 가능성이 있다.

(아래 블로그에 따르면 line 단위로 읽는 경우 기존 io 방식이 더 빠를 수 있다고 한다.) https://taes-k.github.io/2021/01/06/java-nio/

Directed Byte Buffer

기존 JAVA IO에서는 Buffer가 JVM의 Heap 영역에서 생성이 된다. 파일 시스템을 통해서 데이터를 읽어오면, 시스템 커널에 해당 내용에 대한 버퍼가 생성되고, 그 버퍼의 내용을 복사해서 JVM에 생성을 한다.

하지만 NIO의 Buffer는 Direct Buffer로 생성이 가능하다. Direct Buffer에 대한 설명은 Java Docs의 내용을 가져왔다.

Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations. Direct Byte Buffer가 주어지면 Java 가상 머신은 네이티브 I/O 연산을 직접 수행하기 위해 최선의 노력을 기울입니다. 즉, 기본 운영 체제의 네이티브 I/O 연산 중 하나를 호출하기 전(또는 호출 후)에 버퍼의 콘텐츠를 중간 버퍼로 복사하지 않으려고 시도합니다. - https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/ByteBuffer.html

위의 내용에서 버퍼의 콘텐츠를 중간 버퍼로 복사하지 않으려고 시도합니다. 라는 말이 앞서서 설명한 non-direct 방식의 buffer이고, 일반적인 Java IO에서 나타나는 방식이다.

The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. 이 메서드가 반환하는 버퍼는 일반적으로 직접 버퍼가 아닌 버퍼보다 할당 및 할당 해제 비용이 다소 높습니다.

위의 말은 Direct Byte Buffer가 JVM의 메모리가 아닌 시스템의 메모리를 이용하기 때문에, 할당, 해제의 오버헤드가 더 높다는 것을 의미한다.

이 내용을 토대로 정리하자면, NIO에서 이용하는 Direct Byte Buffer는 시스템 메모리에서 생성이 되어, 네이티브 수준의 I/O가 가능하기 때문에, 할당,해제 오버헤드가 발생해도 I/O의 성능 자체는 훌륭하다는 것을 알 수 있다.

논블로킹

NIO가 성능을 향상시키는 가장 큰 요인 중 하나인 논블로킹은, NIO에서 이용하는 Channel 중 비동기통신이 가능한 클래스에서 이용가능하다.

먼저 Channel이란 NIO패키지에 존재하는 Stream과 다른 새로운 통신 방법이다. Channel은 Stream과 달리 양방향 통신이 가능한 방법으로, 파라미터 설정을 통해 그 방향을 설정할 수 있다.

논블로킹을 이용할 수 있는 채널로는 AsynchronousChannel 의 구현체들이 있다. AsynchronousChannel의 Docs를 보면 아래와 같이 나와있다.

A channel that supports asynchronous I/O operations. Asynchronous I/O operations will usually take one of two forms:

Future<V> operation(...)
void operation(... A attachment, CompletionHandler<V,? super A> handler)

Future 객체를 받아서 비동기 처리를 하거나, CompletionHandler를 구현하여 성공할 경우와 실패할 경우에 대한 처리를 해주는 것으로 논블로킹 기능을 이용할 수 있다.

적용 사례

사실 nio의 Files를 이용하면 대체로 좋은 성능을 보이고, 이용하기에 편하기 때문에 IO를 이용할 일이 있다면 적용하는 것이 좋아보인다.

특별히 nio의 논블로킹 기능을 이용한 사례를 본다면, 책에 나온 WatcherService를 이용한 파일 변경관리, Selector를 이용한 여러개의 Channel을 관리하는 방법이 있다.

아래와 같은 그림에서 Channel의 상태를 모니터링하게 될 경우 다수의 Thread가 소모된다.

하지만 아래와 같이 Selector를 두게 된다면, Selector가 Channel에서 비동기적으로 요청 혹은 반응이 올 때, 적절하게 응답을 한다면, 하나의 Thread로 효과적으로 관리할 수 있다.

Selector 예시 디테일

결론

Java NIO는 Java IO의 불편함을 해소하기 위해 등장한 패키지로, 성능과 편의성을 향상시켜준다. NIO에서 이용하는 Channel이라는 양방향 통신이 가능한 Buffer 기반의 객체를 이용한다면 Stream보다 뛰어난 성능을 보일 수 있다. Channel의 논블로킹 기능을 이용한다면, 성능을 더욱 향상시킬 수 있을 것이다.

참고 자료

https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/ByteBuffer.html https://docs.oracle.com/javase/tutorial/essential/io/streams.html https://otrodevym.tistory.com/entry/java-%EA%B8%B0%EC%B4%88-17-IO-%EA%B8%B0%EB%B0%98-%EC%9E%85%EC%B6%9C%EB%A0%A5 https://brewagebear.github.io/fundamental-nio-and-io-models/ https://hbase.tistory.com/39 https://dev-coco.tistory.com/42 https://dev-coco.tistory.com/43 Practical 모던 자바 7장. 파일I/O(NIO2.0)

BufferReader보다 느린 것..

    static ReadableByteChannel in = Channels.newChannel(System.in);
    static ByteBuffer buffer = ByteBuffer.allocate(100000000); // 버퍼 크기

// ...

       st = new StringTokenizer(readAll(), "\r\n");

        liner = new StringTokenizer(st.nextToken());

//...
    private static String readAll() throws IOException { // 왜 다 읽어버리니..
        in.read(buffer);
        buffer.flip(); // limit 은 포지션 position은 0으로 이동.
        Charset charset = Charset.defaultCharset();
        return charset.decode(buffer).toString();
    }

아래 두 성공했습니다는 BufferReader, CustomerReader, 위에 2개는 Channel 이용했을 경우..

image