Java - File 읽고(Read) 출력(Write)하기

Java에서 파일을 입력하고, 출력해보는 예제이다.

Java에서 파일을 입력하기 위해서는 FileInputStream 클래스를 사용해야 한다. 마찬가지로 입력받은 파일을 다시 새로운 파일로 생성하기 위해서는 FileOutputStream 클래스를 사용해야 한다.

사용하기 전 먼저 초기화를 해준다.

1
2
FileInputStream fis = null;
FileOutputStream fos = null;

이제 FileInputStream 클래스로 생성한 인스턴스에 입력할 대상 파일의 경로(path)를 지정해주어야 한다.

그런데 이렇게 exception이 발생할 수 있다고 경고가 발생하고 있다. FileNotFoundException이 발생할 수 있기 때문에 try-cat문으로 예외처리를 해준다.

File 입력 인스턴스 아래에 출력할 파일의 경로도 지정해준다.

1
2
3
4
5
6
try {
fis = new FileInputStream("src/io/helloworld.txt");
fos = new FileOutputStream("src/io/cloneHello.txt");
} catch (Exception e){
e.printStackTrace();
}

입력대상 파일은 미리 만들어주자.

1
2
Hello World!
My name is Devandy.

입출력 관련 인스턴스를 생성했으므로, 이제 파일을 입력하는 코드를 작성해보자. 파일을 입력하기 위해서는 FileInputStream 클래스의 read()를 사용해서 입력할 수 있다.

Eclipse에서 command(windows: control) 키를 누르고 read()를 클릭해서 확인해보니, byte 단위로 입력을 받으며, 더 이상 읽을 데이터가 없을 때 -1을 출력하는걸 알 수 있다.

그럼 read()가 -1을 반환할 때까지 반복문으로 파일을 입력시키면 될 것 같다. 한 byte 단위로 입력하기 때문에 출력하는 시점도 byte 단위로 읽어들이는 그 순간, 순간 해주어야 한다.

1
2
3
4
int readData = 0;
while((readData=fis.read()) != -1){
fos.write(readData);
}

int형 변수 readDataFileInputStream 클래스의 read()로 읽어들인 byte를 저장하는 임시 공간역할을 한다. readData에 byte가 담겨지면, 곧바로 FileOutputStream 클래스의 write()를 이용하여 readData에 담긴 byte를 출력한다. 이러한 행위를 fis.read()를 통해 readData에 -1이 들어올때까지 반복한다. fis.read()에 -1이 들어왔다는건 더 이상 읽어들일 byte가 없다는 의미라는걸 앞에서 확인했다.

파일 입력이 끝나면, 끝났다는걸 콘솔에 알려줄 print문 하나를 while문 아래에 추가하자.

1
2
3
if(readData==-1) {
System.out.println("Read Done!!");
}

최종 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io;

import java.io.FileInputStream;
import java.io.FileOutputStream;

public class DevAndyExamIO {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;

try {
fis = new FileInputStream("src/io/helloworld.txt");
fos = new FileOutputStream("src/io/cloneHello");

int readData = 0;
while((readData=fis.read())!=-1) {
fos.write(readData);
}

if(readData==-1) {
System.out.println("Read Done!!");
}

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

FileInputStream,FileOutputStream 클래스의 인스턴스의 쓰임이 다했다면, close()로 클래스를 닫아주어야한다.

Stream 인스턴스를 닫아주어야 하는 이유를 공식문서에서는 다음과 같이 설명하고 있다.

Streams have a BaseStream.close() method and implement AutoCloseable, but nearly all stream instances do not actually need to be closed after use. Generally, only streams whose source is an IO channel (such as those returned by Files.lines(Path, Charset)) will require closing. Most streams are backed by collections, arrays, or generating functions, which require no special resource management. (If a stream does require closing, it can be declared as a resource in a try-with-resources statement.)

모든 stream 인스턴스는 닫아줄 필요가 없긴 하지만, IO 관련 stream 클래스의 경우는 닫아주어야 한다. 그러나 명확한 설명은 작성되어 있지 않은데, 그래서 구글링을 해보니 10여년전, 이미 이것에 대해 OKKY에서 여러 의견이 오고갔음을 확인했다.

요약하면, GC가 메모리 관리를 해주긴 하지만, GC가 인스턴스를 언제 종료시킬지 알 수 없기 때문에 사용이 끝난 시점에 개발자가 수동으로 종료해주는게 좋은듯하다.

try-cat문의 finally를 추가하여 close()를 작성할 수 있지만, 공식문서에서 try-with-resources를 추천했으니 이걸로 수정해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try (
FileInputStream fis = new FileInputStream("src/io/helloworld.txt");
FileOutputStream fos = new FileOutputStream("src/io/cloneHello.txt");
) {
int readData = 0;
while((readData=fis.read())!=-1) {
fos.write(readData);
}
if(readData==-1) {
System.out.println("Read Done.!!");
}
} catch(Exception e) {
e.printStackTrace();
}

이렇게 try-with-resources를 사용하면, try{}문 안에 작성된 코드가 모두 실행되면 자동으로 try문에 선언된 객체의 close()를 실행시킨다고 한다.

이제 main 메서드를 실행하면, 콘솔에 메세지와 함께 디렉토리에 helloworld.txt를 복사한 cloneHello.txt가 생성될 것이다. Eclipse에선 즉시 파일이 생기지 않으므로 io 패키지를 refresh 해야 생성된 파일을 볼 수 있다.


이렇게 해도 파일을 입력하고, 출력할 수 있지만, 한 byte씩 입출력하는건 효율적이지 않다. 입력을 한 번에 많이 받아두고, 한 번에 출력하면 입출력 성능이 더 좋아질것 같다. 이를 위해 buffer 개념을 적용해서 출력해보았다.

1
2
3
4
5
int readData = 0;
byte[] buffer = new byte[512];
while(fis.read()!=-1) {
fos.write(fis.read());
}

이렇게 해야하는 이유는 기존의 한 byte씩만 입출력하는것보다 효율적이기 때문인데 왜냐면, 개발자가 한 byte씩 입출력을 하더라도 운영체제는 512 byte씩 InputStream클래스에서 가져와서 OutputStream으로 가져가기 때문이다.

마치 512명을 태울수 있는 비행기가 한 명만 태우고 비행하는 것과 같은 개념이다. 한 번에 512명을 모투 태워서 비행한다면, 비행 회수가 줄어들것이다. 이 방식으로 buffer를 만들면 입출력 성능이 개선될 수 있는 것이다.