⬛ 파일에 데이터 저장하기
데이터를 파일에 저장하기 위해서는 FileOutputStream을, 파일에서 데이터를 읽기 위해서는 FileInputStream을 사용한다.
write()와 read() 메서드를 통해 데이터를 바이트 단위로 쓰고 읽을 수 있으며, 파일의 끝에 도달하면 -1을 반환한다.
public class StreamStartMain2 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("temp/hello.dat");
fos.write(65);
fos.write(66);
fos.write(67);
fos.close();
FileInputStream fis = new FileInputStream("temp/hello.dat");
int data;
while ((data = fis.read()) != -1) {
System.out.println("data = " + data);
}
fis.close();
}
}
아래처럼, byte[]를 사용해 데이터를 원하는 크기만큼 더 편리하게 저장하고 읽을 수 있다.
public class StreamStartMain3 {
//byte[] 을 사용해서 데이터를 원하는 크기 만큼 더 편리하게 저장
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("temp/hello.dat");
byte[] input = {65, 66, 67};
fos.write(input);
fos.close();
FileInputStream fis = new FileInputStream("temp/hello.dat");
byte[] buffer = new byte[10];
int readCount = fis.read(buffer, 0, 10);
System.out.println("readCount = " + readCount);
System.out.println(Arrays.toString(buffer));
fis.close();
}
}
⬛ 버퍼 사용의 중요성
아래처럼 데이터를 하나씩 전달하면, 14초정도의 실행시간이 걸린다.
public class CreateFileV1 {
// 1byte씩 디스크에 데이터를 전달
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
for (int i = 0; i < FILE_SIZE; i++) {
fos.write(1);
}
fos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB" );
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
왜 이리 오래걸릴까?
write(),read()는 OS 시스템콜로 명령어를 전달한다.이는 상대적으로 무거운작업인데, 이를 1000만번 반복하니 느려질 수 밖에 없다.
(비유를 하자면 창고에서 마트까지 상품을 전달해야 하는데, 화물차에 한 번에 하나의 물건만 가지고 이동하는 것...!)
이를 해결하기 위해서는 시스템 콜 횟수를 줄여야한다. 버퍼를 두고 byte[]배열에 담아 한번에 여러 byte를 전달할 수 있다.
public class CreateFileV2 {
// byte[] 배열에 담아 한번에 여러 byte 전달
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
long startTime = System.currentTimeMillis();
byte[] buffer = new byte[BUFFER_SIZE];
int bufferIndex = 0;
for (int i = 0; i < FILE_SIZE; i++) {
buffer[bufferIndex++] = 1;
//버퍼가 가득 차면 쓰고, 버퍼를 비운다.
if (bufferIndex == BUFFER_SIZE) {
fos.write(buffer); // BUFFER_SIZE 만큼 데이터를 모아서 write() 를 호출
bufferIndex = 0;
}
}
// 끝 부분에 오면 버퍼가 가득차지 않고 남아있을 수 있다. 버퍼에 남은 부분 쓰기
if (bufferIndex > 0) {
fos.write(buffer, 0, bufferIndex);
}
fos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
BufferedOutputStream과 BufferedInputStream을 사용하면 내부적으로 버퍼링을 처리하여 더 효율적인 입출력을 구현할 수 있다.
이는 마치 물건을 하나씩 옮기기보다는 한 상자에 가득 담아서 한 번에 옮기는 것과 비슷하다.
public class CreateFileV3 {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream(FILE_NAME);
BufferedOutputStream bos = new BufferedOutputStream(fos, BUFFER_SIZE); // 사용할 버퍼크기 전달
long startTime = System.currentTimeMillis();
for (int i = 0; i < FILE_SIZE; i++) {
bos.write(1);
}
bos.close();
long endTime = System.currentTimeMillis();
System.out.println("File created: " + FILE_NAME);
System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB");
System.out.println("Time taken: " + (endTime - startTime) + "ms");
}
}
여기서 주의할점은, 버퍼 크기를 무한정 늘린다고 속도가 비례해 계속 줄어들지 않는다는점이다.
디스크나 파일시스템의 읽고 쓰는 기본단위는 4KB 또는 8KB이다.
또다른 주의할 점은, 반드시 마지막에 연결한 스트림을 닫아야 한다는 것이다. bufferedOutputStream을 close()로 닫으면 내부에서 flush()를 호출하고 비운 후 자원을 정리한다. 따라서 꼭 이를 호출해주어야한다.
🟢 정리
- BufferedOutputStream 은 버퍼 기능을 제공하는 보조 스트림이다.
- BufferedOutputStream 도 OutputStream의 자식이기 때문에 OutputStream의 기능을 그대로 사용할 수 있다.
- 물론 대부분의 기능은 재정의 된다. write()의 경우 먼저 버퍼에 쌓도록 재정의 된다.
- 버퍼의 크기만큼 데이터를 모아서 전달하기 때문에 빠른 속도로 데이터를 처리할 수 있다.
⬛ 기본 스트림 vs 보조 스트림
FileOutputStream과 같이 단독으로 사용할 수 있는 스트림을 "기본 스트림"이라고 하며, BufferedOutputStream과 같이 보조 기능을 제공하는 스트림은 "보조 스트림"이라고 한다.
⬛ BufferedXxx 클래스의 특징
BufferedXxx 클래스는 자바 초창기에 만들이진 클래스인데, 처음부터 멀티 스레드를 고려해서 만든 클래스이다.
따라서 멀티 스레드에 안전하지만 락을 걸고 푸는 동기화 코드로 인해 성능이 약간 저하될 수 있다.
매우 큰 데이터를 다루어야 하고, 성능 최적화가 중요하다면 직접 버퍼를 다루는 방법을 사용해야한다.
🟢 정리
- 파일의 크기가 크지 않아서, 메모리 사용에 큰 영향을 주지 않는다면 쉽고 빠르게 한 번에 처리하자.
- 성능이 중요하고 큰 파일을 나누어 처리해야 한다면, 버퍼를 직접 다루자.
- 성능이 크게 중요하지 않고, 버퍼 기능이 필요하면 BufferedXxx 를 사용하자.
- BufferedXxx 는 동기화 코드가 들어있어서 스레드 안전하지만, 약간의 성능 저하가 있다.
⬛ 문자 저장하기
앞서 컴퓨터는 문자가 아니라 byte만 이해할 수 있다고했다. 문자를 파일에 저장하기 위해서 String.getBytes(Charset)을 통해 문자를 byte로 변환하여, FileOutputStream을 사용해 파일에 저장할 수 있다.
🔶 문자 변환 저장 예시 코드
public class ReaderWriterMainV1 {
public static void main(String[] args) throws IOException {
String writeString = "ABC";
// 문자 -> byte 변환, UTF-8 인코딩
byte[] writeBytes = writeString.getBytes(UTF_8);
System.out.println("write String: " + writeString);
System.out.println("write bytes: " + Arrays.toString(writeBytes));
// 파일에 쓰기
FileOutputStream fos = new FileOutputStream(FILE_NAME);
fos.write(writeBytes);
fos.close();
// 파일에서 읽기
FileInputStream fis = new FileInputStream(FILE_NAME);
byte[] readBytes = fis.readAllBytes();
fis.close();
// byte -> String UTF-8 디코딩
String readString = new String(readBytes, UTF_8);
System.out.println("read bytes: " + Arrays.toString(writeBytes));
System.out.println("read String: " + readString);
}
}
매번 문자를 byte로 바꾸는 게 귀찮은데, 좀 더 쉽게 저장할 방법이 없을까?
InputStreamReader와 OutputStreamWriter 이 클래스들은 문자를 바로 읽고 쓸 수 있도록 도와준다.
- OutputStreamWriter는 문자를 byte로 변환하여 OutputStream에 전달
- InputStreamReader는 파일에서 읽은 byte를 문자로 자동 변환
public class ReaderWriterMainV2 { // 스트림에 byte대신 문자 저장,읽기 지원
public static void main(String[] args) throws IOException {
String writeString = "ABC";
System.out.println("write String: " + writeString);
// 파일에 쓰기
FileOutputStream fos = new FileOutputStream(FILE_NAME);
//문자 -> byte[]로 변환
OutputStreamWriter osw = new OutputStreamWriter(fos, UTF_8);
osw.write(writeString);
osw.close();
// 파일에서 읽기
FileInputStream fis = new FileInputStream(FILE_NAME);
// byte[] -> 문자로 변환
InputStreamReader isr = new InputStreamReader(fis, UTF_8);
StringBuilder content = new StringBuilder();
int ch;
while ((ch = isr.read()) != -1) {
content.append((char) ch);
}
isr.close();
System.out.println("read String: " + content);
}
}
⬛ Reader, Writer
모든 데이터는 byte 단위(숫자)로 저장된다.
자바는 byte를 다루는 I/O 클래스와 문자를 다루는 I/O 클래스를 둘로 나누어두었다.
보통 byte를 다루는 클래스는 Stream으로 클래스가 끝나고, 문자를 다루는 클래스는 Writer,Reader가 끝에 붙는다.
Reader와 Writer가 byte와 문자의 변환을 자동으로 처리하여 파일 입출력을 훨씬 쉽게 만들어준다. 내부적으로는 문자를 byte로 변경해 결국 Stream으로 데이터를 전달한다.
⬛ 한 줄씩 효율적으로 파일 읽기 - BufferedReader와 BufferedWriter
BufferedReader는 readLine() 메서드를 통해 한 줄씩 읽을 수 있게 돕고, 빠른 입출력을 위해 버퍼를 사용해 성능을 높인다.BufferedWriter는 빠르게 내용을 파일에 기록하게 해준다.
public class ReaderWriterMainV4 {
private static final int BUFFER_SIZE = 8192;
public static void main(String[] args) throws IOException {
String writeString = "ABC\n가나다";
System.out.println("== writeString ==");
System.out.println(writeString);
// 파일에 쓰기
FileWriter fw = new FileWriter(FILE_NAME, UTF_8);
BufferedWriter bw = new BufferedWriter(fw, BUFFER_SIZE);
bw.write(writeString);
bw.close();
// 파일에서 읽기
StringBuilder content = new StringBuilder();
FileReader fr = new FileReader(FILE_NAME, UTF_8);
BufferedReader br = new BufferedReader(fr, BUFFER_SIZE);
String line;
while ((line = br.readLine()) != null) {
content.append(line).append("\n");
}
br.close();
System.out.println(" == Read String == ");
System.out.println(content);
}
}
⬛ 다양한 스트림
PrintStream: 마치 콘솔에 출력하듯이 파일에 문자열을 쉽게 출력할 수 있다. System.out처럼 PrintStream을 통해 파일에 데이터를 쉽게 출력할 수 있다.
public class PrintStreamEtcMain {
public static void main(String[] args) throws FileNotFoundException {
FileOutputStream fos = new FileOutputStream("temp/print.txt");
PrintStream printStream = new PrintStream(fos); // 파일에 문자출력
printStream.println("hello java!");
printStream.println(10);
printStream.println(true);
printStream.printf("hello %s", "world");
printStream.close();
}
}
DataOutputStream: int, boolean 같은 다양한 데이터 형식을 저장할 때 사용한다. 순서대로 저장한 후, 읽을 때도 반드시 같은 순서로 읽어야 한다.
public class DataStreamEtcMain {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("temp/data.dat");
DataOutputStream dos = new DataOutputStream(fos);
dos.writeUTF("회원A");
dos.writeInt(20);
dos.writeDouble(10.5);
dos.writeBoolean(true);
dos.close();
FileInputStream fis = new FileInputStream("temp/data.dat");
DataInputStream dis = new DataInputStream(fis);
System.out.println(dis.readUTF());
System.out.println(dis.readInt());
System.out.println(dis.readDouble());
System.out.println(dis.readBoolean());
dis.close();
}
}
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 고급 2편 섹션5 네트워크 기본이론 / HTTP (2) | 2024.10.31 |
---|---|
[Java] 김영한의 실전 자바 - 고급 2편 섹션5 File,Files (0) | 2024.10.28 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션12 스레드 풀과 Executor 프레임워크2 (1) | 2024.09.30 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션12 스레드 풀과 Executor 프레임워크 (0) | 2024.09.29 |
[Java] 김영한의 실전 자바 - 고급 1편 섹션9 생산자 소비자 문제2 (1) | 2024.09.26 |