Java IO가 나쁜 이유는 무엇입니까?

IO(Input & Output) API는 개발자가 스트림 작업을 쉽게 할 수 있도록 하는 Java API입니다. 일부 데이터(예: 이름, 중간 이름, 성)를 수신하고 이를 파일에 작성해야 한다고 가정해 보겠습니다. 이제 java.io 를 사용할 때가 되었습니다 .

java.io 라이브러리의 구조

그러나 Java IO 에는 단점이 있으므로 각 단점에 ​​대해 차례로 이야기해 보겠습니다.

  1. 입/출력에 대한 액세스를 차단합니다. 문제는 개발자가 Java IO를 사용하여 파일에 무언가를 읽거나 쓰려고 할 때 작업이 완료될 때까지 파일을 잠그고 액세스를 차단한다는 것입니다.
  2. 가상 파일 시스템을 지원하지 않습니다.
  3. 링크를 지원하지 않습니다.
  4. 확인된 예외가 많이 있습니다.

파일 작업에는 항상 예외 작업이 수반됩니다. 예를 들어 이미 존재하는 새 파일을 만들려고 하면 IOException 이 발생 합니다 . 이 경우 응용 프로그램은 계속 실행되어야 하며 파일을 만들 수 없는 이유를 사용자에게 알려야 합니다.


try {
	File.createTempFile("prefix", "");
} catch (IOException e) {
	// Handle the IOException
}

/**
 * Creates an empty file in the default temporary-file directory 
 * any exceptions will be ignored. This is typically used in finally blocks. 
 * @param prefix 
 * @param suffix 
 * @throws IOException - If a file could not be created
 */
public static File createTempFile(String prefix, String suffix) 
throws IOException {
...
}

여기에서 파일을 생성할 수 없을 때 createTempFile 메서드가 IOException을 발생시키는 것을 볼 수 있습니다 . 이 예외는 적절하게 처리되어야 합니다. try-catch 블록 외부에서 이 메서드를 호출하려고 하면 컴파일러는 오류를 생성하고 오류를 수정하기 위한 두 가지 옵션을 제안합니다. 메서드를 try-catch 블록으로 래핑하거나 File.createTempFile을 호출하는 메서드가 IOException을 발생시키 도록 합니다 ( 더 높은 수준에서 처리할 수 있습니다.)

Java NIO에 도달하고 Java IO와 비교하는 방법

Java NIO 또는 Java Non-Blocking I/O(또는 때때로 Java New I/O)는 고성능 I/O 작업을 위해 설계되었습니다.

Java IO 메서드와 이를 대체하는 메서드를 비교해 봅시다 .

먼저 Java IO 작업에 대해 이야기해 보겠습니다 .

InputStream 클래스


try(FileInputStream fin = new FileInputStream("C:/codegym/file.txt")){
    System.out.printf("File size: %d bytes \n", fin.available());
    int i=-1;
    while((i=fin.read())!=-1) {
        System.out.print((char)i);
    }   
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

FileInputStream 클래스는 파일에서 데이터를 읽기 위한 것입니다 . InputStream 클래스를 상속하므로 모든 메서드를 구현합니다. 파일을 열 수 없으면 FileNotFoundException 이 발생합니다.

출력 스트림 클래스


String text = "Hello world!"; // String to write
try(FileOutputStream fos = new FileOutputStream("C:/codegym/file.txt")){
    // Convert our string into bytes
    byte[] buffer = text.getBytes();
    fos.write(buffer, 0, buffer.length);
    System.out.println("The file has been written");
} catch(IOException ex) {
    System.out.println(ex.getMessage());
}

파일에 바이트를 쓰기 위한 FileOutputStream 클래스 입니다 . OutputStream 클래스 에서 파생됩니다 .

리더 및 라이터 클래스

FileReader 클래스를 사용하면 스트림에서 문자 데이터를 읽을 수 있고 FileWriter 클래스 문자 스트림을 작성하는 데 사용됩니다. 다음 코드는 파일에서 읽고 쓰는 방법을 보여줍니다.


        String fileName = "c:/codegym/Example.txt";

        // Create a FileWriter object
        try (FileWriter writer = new FileWriter(fileName)) {

            // Write content to file
            writer.write("This is a simple example\nin which we\nwrite to a file\nand read from a file\n");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Create a FileReader object
        try (FileReader fr = new FileReader(fileName)) {
            char[] a = new char[200]; // Number of characters to read
            fr.read(a); // Read content into an array
            for (char c : a) {
                System.out.print(c); // Display characters one by one
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

이제 Java NIO 에 대해 이야기해 보겠습니다 .

채널

Java IO 에서 사용되는 스트림과 달리 채널은 양방향 인터페이스입니다. 즉, 읽기와 쓰기가 모두 가능합니다 . Java NIO 채널은 차단 모드와 비차단 모드 모두에서 비동기 데이터 흐름을 지원합니다.


RandomAccessFile aFile = new RandomAccessFile("C:/codegym/file.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(100);
int bytesRead = inChannel.read(buf);

while (bytesRead != -1) {
  System.out.println("Read: " + bytesRead);
  buf.flip();
	  while(buf.hasRemaining()) {
	      System.out.print((char) buf.get());
	  }
  buf.clear();
  bytesRead = inChannel.read(buf);
}
aFile.close();

여기서는 FileChannel 을 사용했습니다 . 파일 채널을 사용하여 파일에서 데이터를 읽습니다. 파일 채널 객체는 파일 객체에서 getChannel() 메서드를 호출해야만 만들 수 있습니다 . 파일 채널 객체를 직접 만들 수 있는 방법은 없습니다.

FileChannel 외에도 다른 채널 구현이 있습니다.

  • FileChannel — 파일 작업용

  • DatagramChannel — UDP 연결을 통해 작업하기 위한 채널

  • SocketChannel — TCP 연결을 통해 작동하는 채널

  • ServerSocketChannel은 SocketChannel을 포함하며웹 서버 작동 방식과 유사합니다.

참고: FileChannel은 비차단 모드로 전환할 수 없습니다. Java NIO 의 비차단 모드를 사용하면 채널에서 읽기 데이터를 요청하고 현재 사용 가능한 데이터만 수신할 수 있습니다(또는 아직 사용 가능한 데이터가 없는 경우 아무것도 수신하지 않음). 즉, SelectableChannel 및 해당 구현은 connect() 메서드 를 사용하여 비차단 모드로 설정할 수 있습니다 .

선택자

Java NIO는 어떤 채널이 데이터를 쓰고 읽을 준비가 되었는지 알고 해당 특정 채널을 처리할 수 있는 스레드를 생성하는 기능을 도입했습니다. 이 기능은 Selector 클래스를 사용하여 구현됩니다.

선택기에 채널 연결


Selector selector = Selector.open();
channel.configureBlocking(false); // Non-blocking mode
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

그래서 Selector를 생성 하고 SelectableChannel 에 연결합니다 .

선택기와 함께 사용하려면 채널이 비차단 모드여야 합니다. 이는 FileChannel을 비차단 모드로 전환할 수 없기 때문에 선택기와 함께 FileChannel 을 사용할 수 없음을 의미합니다. 그러나 소켓 채널은 잘 작동합니다.

여기서 우리의 예에서 SelectionKey 는 채널에서 수행할 수 있는 일련의 작업임을 언급하겠습니다 . 선택 키는 채널의 상태를 알려줍니다.

선택키의 종류

  • SelectionKey.OP_CONNECT는 서버에 연결할 준비가 된 채널을 나타냅니다.

  • SelectionKey.OP_ACCEPT 는 들어오는 연결을 수락할 준비가 된 채널입니다.

  • SelectionKey.OP_READ는 데이터를 읽을 준비가 된 채널을 나타냅니다.

  • SelectionKey.OP_WRITE는 데이터를 쓸 준비가 된 채널을 나타냅니다.

완충기

추가 처리를 위해 데이터를 버퍼로 읽습니다. 개발자는 버퍼에서 앞뒤로 이동할 수 있으므로 데이터를 처리할 때 좀 더 유연하게 사용할 수 있습니다. 동시에 버퍼에 올바른 처리에 필요한 데이터 양이 포함되어 있는지 확인해야 합니다. 또한 데이터를 버퍼로 읽을 때 아직 처리되지 않은 기존 데이터를 파괴하지 않도록 하십시오.


ByteBuffer buf = ByteBuffer.allocate (2048); 
int bytesRead = channel.read(buf);
buf.flip(); // Change to read mode
while (buf.hasRemaining()) { 
	byte data = buf.get(); // There are methods for primitives 
}

buf.clear(); // Clear the buffer - now it can be reused

버퍼의 기본 속성:

기본 속성
용량 배열의 길이인 버퍼 크기입니다.
위치 데이터 작업을 위한 시작 위치입니다.
한계 작동 제한입니다. 읽기 작업의 경우 한도는 읽을 수 있는 데이터의 양이지만 쓰기 작업의 경우 쓰기에 사용할 수 있는 용량 또는 할당량입니다.
표시 reset() 메서드가 호출될 때 position 매개 변수가 재설정될 값의 인덱스입니다 .

이제 Java NIO.2 의 새로운 기능에 대해 조금 이야기해 보겠습니다 .

경로는 파일 시스템의 경로를 나타냅니다. 여기에는 파일 이름과 경로를 정의하는 디렉토리 목록이 포함됩니다.


Path relative = Paths.get("Main.java");
System.out.println("File: " + relative);
// Get the file system
System.out.println(relative.getFileSystem());

Paths는 단일 정적 메소드인 get() 이 있는 매우 단순한 클래스입니다 . 전달된 문자열이나 URI에서 Path 객체를 얻기 위해서만 생성되었습니다


Path path = Paths.get("c:\\data\\file.txt");

파일

파일은 파일 크기를 직접 가져오고 파일을 복사하는 등의 작업을 수행할 수 있는 유틸리티 클래스입니다.


Path path = Paths.get("files/file.txt");
boolean pathExists = Files.exists(path);

파일 시스템

FileSystem은 파일 시스템에 대한 인터페이스를 제공합니다. FileSystem 은 다양한 객체(,PathMatcher,파일). 파일 시스템의 파일 및 기타 개체에 액세스하는 데 도움이 됩니다.


try {
      FileSystem filesystem = FileSystems.getDefault();
      for (Path rootdir : filesystem.getRootDirectories()) {
          System.out.println(rootdir.toString());
      }
  } catch (Exception e) {
      e.printStackTrace();
  }

성능 검사

이 테스트를 위해 두 개의 파일을 사용하겠습니다. 첫 번째는 작은 텍스트 파일이고 두 번째는 큰 비디오입니다.

파일을 만들고 몇 가지 단어와 문자를 추가합니다.

% 터치 text.txt

파일은 메모리에서 총 42바이트를 차지합니다.

이제 한 폴더에서 다른 폴더로 파일을 복사하는 코드를 작성해 보겠습니다. IONIONIO.2 의 속도를 비교하기 위해 작은 파일과 큰 파일에서 테스트해 봅시다 .

Java IO를 사용하여 작성된 복사용 코드 :


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;
        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text1.txt");
        copyFileByIO(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByIO(File src, File dst) {
        try(InputStream inputStream = new FileInputStream(src);
            OutputStream outputStream = new FileOutputStream(dst)){

            byte[] buffer = new byte[1024];
            int length;
            // Read data into a byte array and then output to an OutputStream
            while((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

다음은 Java NIO 용 코드입니다 .


public static void main(String[] args) {
        long currentMills = System.currentTimeMillis();
        long startMills = currentMills;

        File src = new File("/Users/IdeaProjects/testFolder/text.txt");
        File dst = new File("/Users/IdeaProjects/testFolder/text2.txt");
        // Code for copying using NIO
        copyFileByChannel(src, dst);
        currentMills = System.currentTimeMillis();
        System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
    }

    public static void copyFileByChannel(File src, File dst) {
        // 1. Get a FileChannel for the source file and the target file
        try(FileChannel srcFileChannel  = new FileInputStream(src).getChannel();
            FileChannel dstFileChannel = new FileOutputStream(dst).getChannel()){
            // 2. Size of the current FileChannel
            long count = srcFileChannel.size();
            while(count > 0) {
                /**=============================================================
                 * 3. Write bytes from the source file's FileChannel to the target FileChannel
                 * 1. srcFileChannel.position(): the starting position in the source file, cannot be negative
                 * 2. count: the maximum number of bytes transferred, cannot be negative
                 * 3. dstFileChannel: the target file
                 *==============================================================*/
                long transferred = srcFileChannel.transferTo(srcFileChannel.position(),
                        count, dstFileChannel);
                // 4. After the transfer is complete, change the position of the original file to the new one
                srcFileChannel.position(srcFileChannel.position() + transferred);
                // 5. Calculate how many bytes are left to transfer
                count -= transferred;
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Java NIO.2 용 코드 :


public static void main(String[] args) {
  long currentMills = System.currentTimeMillis();
  long startMills = currentMills;

  Path sourceDirectory = Paths.get("/Users/IdeaProjects/testFolder/test.txt");
  Path targetDirectory = Paths.get("/Users/IdeaProjects/testFolder/test3.txt");
  Files.copy(sourceDirectory, targetDirectory);

  currentMills = System.currentTimeMillis();
  System.out.println("Execution time in milliseconds: " + (currentMills - startMills));
}

작은 파일부터 시작하겠습니다.

Java IO 의 실행 시간은 평균 1밀리초였습니다. 테스트를 여러 번 실행하면 0에서 2밀리초 사이의 결과를 얻습니다.

실행 시간(밀리초): 1

Java NIO 의 실행 시간은 훨씬 더 깁니다. 평균 시간은 11밀리초입니다. 결과는 9에서 16 사이였습니다. 이는 Java IO가 운영 체제와 다르게 작동하기 때문입니다. IO는 파일을 하나씩 이동하고 처리하지만 운영 체제는 데이터를 하나의 큰 덩어리로 보냅니다. NIO는 IO 처럼 스트림 지향이 아니라 버퍼 지향이기 때문에 성능이 좋지 않았습니다 .

실행 시간(밀리초): 12

그리고 Java NIO.2 에 대한 테스트도 실행해 봅시다 . NIO.2 는 Java NIO 에 비해 파일 관리가 향상되었습니다 . 이것이 업데이트된 라이브러리가 이렇게 다른 결과를 생성하는 이유입니다.

실행 시간(밀리초): 3

이제 큰 파일인 521MB 비디오를 테스트해 보겠습니다. 작업은 정확히 동일합니다. 파일을 다른 폴더에 복사합니다. 바라보다!

Java IO 에 대한 결과 :

실행 시간(밀리초): 1866

다음은 Java NIO 의 결과입니다 .

실행 시간(밀리초): 205

Java NIO는 첫 번째 테스트에서 파일을 9배 더 빠르게 처리했습니다. 반복된 테스트는 거의 동일한 결과를 보여주었습니다.

또한 Java NIO.2 에서 테스트를 시도합니다 .

실행 시간(밀리초): 360

왜 이런 결과가 나왔습니까? 서로 다른 용도로 사용되기 때문에 성능을 비교하는 것이 이치에 맞지 않기 때문입니다. NIO 는 보다 추상적인 저수준 I/O인 반면 NIO.2 는 파일 관리를 지향합니다.

요약

블록 내부 사용 덕분에 파일 작업 시 Java NIO가 훨씬 더 효율적 이라고 안전하게 말할 수 있습니다 . 또 다른 장점은 NIO 라이브러리가 두 부분으로 나누어져 있다는 것입니다 . 하나는 파일 작업용이고 다른 하나는 네트워크 작업용입니다.

파일 작업을 위한 Java NIO.2 의 새로운 API는 많은 유용한 기능을 제공합니다.

  • Path를 사용하여 훨씬 더 유용한 파일 시스템 주소 지정 ,

  • 사용자 지정 파일 시스템 공급자를 사용하여 ZIP 파일 처리를 크게 개선했습니다.

  • 특수 파일 속성에 대한 액세스,

  • 단일 명령문으로 전체 파일 읽기, 단일 명령문으로 파일 복사 등과 같은 많은 편리한 방법

그것은 파일과 파일 시스템에 관한 것이며 모두 꽤 높은 수준입니다.

오늘날 현실은 Java IO 의 점유율이 여전히 중요 하지만 Java NIO가 파일 작업의 대략 80-90%를 차지한다는 것입니다.

💡 PS 이 테스트는 MacBook Pro 14" 16/512에서 실행되었습니다. 테스트 결과는 운영 체제 및 워크스테이션 사양에 따라 다를 수 있습니다.