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

java.io 라이브러리의 구조
그러나 Java IO 에는 단점이 있으므로 각 단점에 대해 차례로 이야기해 보겠습니다.
- 입/출력에 대한 액세스를 차단합니다. 문제는 개발자가 Java IO를 사용하여 파일에 무언가를 읽거나 쓰려고 할 때 작업이 완료될 때까지 파일을 잠그고 액세스를 차단한다는 것입니다.
- 가상 파일 시스템을 지원하지 않습니다.
- 링크를 지원하지 않습니다.
- 확인된 예외가 많이 있습니다.
파일 작업에는 항상 예외 작업이 수반됩니다. 예를 들어 이미 존재하는 새 파일을 만들려고 하면 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();
}
성능 검사
이 테스트를 위해 두 개의 파일을 사용하겠습니다. 첫 번째는 작은 텍스트 파일이고 두 번째는 큰 비디오입니다.
파일을 만들고 몇 가지 단어와 문자를 추가합니다.

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

이제 한 폴더에서 다른 폴더로 파일을 복사하는 코드를 작성해 보겠습니다. IO 와 NIO 및 NIO.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밀리초 사이의 결과를 얻습니다.
Java NIO 의 실행 시간은 훨씬 더 깁니다. 평균 시간은 11밀리초입니다. 결과는 9에서 16 사이였습니다. 이는 Java IO가 운영 체제와 다르게 작동하기 때문입니다. IO는 파일을 하나씩 이동하고 처리하지만 운영 체제는 데이터를 하나의 큰 덩어리로 보냅니다. NIO는 IO 처럼 스트림 지향이 아니라 버퍼 지향이기 때문에 성능이 좋지 않았습니다 .
그리고 Java NIO.2 에 대한 테스트도 실행해 봅시다 . NIO.2 는 Java NIO 에 비해 파일 관리가 향상되었습니다 . 이것이 업데이트된 라이브러리가 이렇게 다른 결과를 생성하는 이유입니다.
이제 큰 파일인 521MB 비디오를 테스트해 보겠습니다. 작업은 정확히 동일합니다. 파일을 다른 폴더에 복사합니다. 바라보다!
Java IO 에 대한 결과 :
다음은 Java NIO 의 결과입니다 .
Java NIO는 첫 번째 테스트에서 파일을 9배 더 빠르게 처리했습니다. 반복된 테스트는 거의 동일한 결과를 보여주었습니다.
또한 Java NIO.2 에서 테스트를 시도합니다 .
왜 이런 결과가 나왔습니까? 서로 다른 용도로 사용되기 때문에 성능을 비교하는 것이 이치에 맞지 않기 때문입니다. 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에서 실행되었습니다. 테스트 결과는 운영 체제 및 워크스테이션 사양에 따라 다를 수 있습니다.
GO TO FULL VERSION