-
[Java] I/O 정리Java 2024. 11. 9. 12:03
- 출력 스트림: 자바 프로세스가 가지고 있는 데이터를 밖으로 보낼 때 사용
- 입력 스트림: 외부 데이터를 자바 프로세스 안으로 가져올 때 사용
- 각 스트림은 단방향으로 흐름
InputStream / OutputStream
- 자바 1.0부터 제공된 추상 클래스
- 이를 상속 받은 다양한 클래스가 제공됨
FileStream
public class StreamStartMain1 { public static void main(String[] args) throws IOException { FileOutputStream fos = new FileOutputStream("temp/hello.dat"); fos.write(65); // int fos.write(66); fos.write(67); fos.close(); FileInputStream fis = new FileInputStream("temp/hello.dat"); System.out.println(fis.read()); // 한 바이트씩 읽기 System.out.println(fis.read()); System.out.println(fis.read()); System.out.println(fis.read()); // -1: EOF 의미 fis.close(); } }
(1) FileOutputStream
FileOutputStream fos = new FileOutputStream("temp/hello.dat"); FileOutputStream fos = new FileOutputStream("temp/hello.dat", true);
- temp 디렉토리를 찾지 못하면 FileNotFoundException 발생
- FileOutputStream은 디렉토리 생성 기능은 제공하지 않음
- hello.dat 파일이 존재하지 않으면 생성함
- hello.dat. 파일이 존재하면 내용을 모두 지우고 작업함 (default)
- 매개변수로 boolean 값을 주어 기존 파일에 추가 작업을 할 것인지, 지우고 작업할 것인지 결정 가능 (default: false)
FileOutputStream fos = new FileOutputStream("temp/hello.dat"); byte[] input = {65, 66, 67}; fos.write(input); // byte[] fos.close();
- write() 매개변수로 byte[]을 받아 한번에 출력 가능
(2) FileInputStream
// 끝까지 읽기 FileInputStream fis = new FileInputStream("temp/hello.dat"); int data; while ((data = fis.read()) != -1) { System.out.println(data); } fis.close();
FileInputStream fis = new FileInputStream("temp/hello.dat"); byte[] buffer = new byte[10]; int readCount = fis.read(buffer, 0, 10); // EOF의 경우 -1 반환 System.out.println("readCount = " + readCount); System.out.println(Arrays.toString(buffer)); fis.close();
- read(byte[] bytes, int offset, int length)
- 메모리 사용량 제어 가능
- 대용량 파일을 처리할 때, 한 번에 메모리에 로드하기보다는 이 메서드를 사용하여 파일을 조각조각 읽어들일 수 있음
- read(byte[] bytes): offset → 0, length → bytes.length
FileInputStream fis = new FileInputStream("temp/hello.dat"); byte[] readBytes = fis.readAllBytes(); System.out.println(Arrays.toString(readBytes)); fis.close();
- readAllBytes() 메서드를 통해 모든 데이터를 한번에 읽어올 수 있다.
- 메모리 사용량을 제어할 수 없어 큰 파일의 경우 OutOfMemoryError가 발생할 수 있다.
ByteArrayStream
public class ByteArrayStreamMain { public static void main(String[] args) throws IOException { byte[] input = {1, 2, 3}; // 메모리에 쓰기 ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(input); // 메모리에서 읽기 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); byte[] bytes = bais.readAllBytes(); System.out.println(Arrays.toString(bytes)); } }
- 메모리에 어떤 데이터를 저장하고 읽을 때는 컬렉션이나 배열을 사용하면 되므로, 이 기능을 잘 사용하지 않음
- 주로 스트림을 간단히 테스트하거나 데이터를 확인하는 용도로 사용
PrintStream
public class PrintStreamMain { public static void main(String[] args) throws IOException { System.out.println("Hello"); PrintStream printStream = System.out; String str = "Hello\\n"; byte[] bytes = str.getBytes(UTF_8); printStream.write(bytes); printStream.println("print!"); } }
- PrintStream은 자바가 시작될 때 자동으로 만들어짐
- OutputStream을 상속 받음
- printStream.write(bytes): println()과 비슷한 기능이지만 write()는 바이트 배열을 직접 사용하며 줄 바꿈이 없음
- println(str): String을 바이트 배열로 변환하여 사용하며 줄바꿈 있음
성능 최적화
(1) 하나씩 쓰기
public class CreateFileV1 { 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"); } }
File created: temp/buffered.dat File size: 10MB Time taken: 11274ms
public class ReadFileV1 { public static void main(String[] args) throws IOException { FileInputStream fis = new FileInputStream(FILE_NAME); long startTime = System.currentTimeMillis(); int fileSize = 0; int data; while ((data = fis.read()) != -1) { fileSize++; } fis.close(); long endTime = System.currentTimeMillis(); System.out.println("File name: " + FILE_NAME); System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB"); System.out.println("Time taken: " + (endTime - startTime) + "ms"); } }
File name: temp/buffered.dat File size: 10MB Time taken: 3860ms
- 오랜 시간이 걸린 이유 : 자바에서 1byte씩 디스크에 데이터를 전달하기 때문
- write(), read() 호출 → 운영체제 시스템 콜 발생 (오버헤드 유발)
(2) 버퍼 활용
public class CreateFileV2 { 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); 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"); } }
File created: temp/buffered.dat File size: 10MB Time taken: 12ms
- 버퍼의 크기가 커진다고 해서 속도가 계속 줄어들지는 않음
- 디스크나 파일 시스템에서 데이터를 읽고 쓰는 기본 단위가 보통 4KB 또는 8KB이기 때문
- 4KB = 4096 byte
- 8KB = 8192 byte
- 결국 버퍼에 많은 데이터를 담아 보내도 디스크나 파일 시스템에서 해당 단위로 나누어 저장하기 때문에 효율에는 한계가 있음
public class ReadFileV2 { public static void main(String[] args) throws IOException { FileInputStream fis = new FileInputStream(FILE_NAME); long startTime = System.currentTimeMillis(); byte[] buffer = new byte[BUFFER_SIZE]; int fileSize = 0; int size; while ((size = fis.read(buffer)) != -1) { fileSize += size; } fis.close(); long endTime = System.currentTimeMillis(); System.out.println("File name: " + FILE_NAME); System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB"); System.out.println("Time taken: " + (endTime - startTime) + "ms"); } }
File name: temp/buffered.dat File size: 10MB Time taken: 3ms
(3) BufferedOutputStream
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(); // flush() 호출, 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"); } }
File created: temp/buffered.dat File size: 10MB Time taken: 63ms
- BufferedOutputStream은 내부에서 버퍼 기능을 제공
- 사용 시 반드시 대상 OutputStream이 있어야 함 (생성자 매개변수로 제공)
- 이와 같이 단독으로 사용할 수 없는 스트림을 보조 스트림이라고 함
- 단독 사용 스트림은 기본 스트림
- 버퍼 크기 조정 가능
- flush() 호출 시 버퍼가 가득차지 않아도 데이터 전달
- close() 호출 시 flush() 호출 → 연관된 스트림 close() 호출 → 자기 자신 close()
- FileOutputStream은 close() 호출할 필요 없으며 FileOutputStream만 close() 하면 안 됨
- BufferedOutputStream 자원 정리가 안 될 뿐더러 flush()가 호출되지 않아서 버퍼 내부에 있는 데이터가 전달되지 않고 사라질 수 있음
(4) BufferedInputStream
public class ReadFileV3 { public static void main(String[] args) throws IOException { FileInputStream fis = new FileInputStream(FILE_NAME); BufferedInputStream bis = new BufferedInputStream(fis, BUFFER_SIZE); long startTime = System.currentTimeMillis(); int fileSize = 0; int data; while ((data = bis.read()) != -1) { fileSize++; } bis.close(); long endTime = System.currentTimeMillis(); System.out.println("File name: " + FILE_NAME); System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB"); System.out.println("Time taken: " + (endTime - startTime) + "ms"); } }
File name: temp/buffered.dat File size: 10MB Time taken: 57ms
- BufferedInputStream은 버퍼의 크기만큼 데이터를 미리 읽어 버퍼에 보관함
- 따라서 read()를 통해 1byte씩 데이터를 조회해도 성능이 최적화됨 (메모리에 접근해서 가져오기 때문)
버퍼를 직접 다룬 예제보다 BufferedXxx 클래스 사용 시 성능이 더 떨어지는 이유는 동기화 때문이다. BufferedXxx 클래스들은 버퍼 사용 부분이 동기화 되어 있기 때문에 멀티 쓰레드에 안전하지만 싱글 쓰레드 상황에서는 오히려 성능이 더 떨어짐
(5) 한번에 쓰기
public class CreateFileV4 { public static void main(String[] args) throws IOException { FileOutputStream fos = new FileOutputStream(FILE_NAME); long startTime = System.currentTimeMillis(); byte[] buffer = new byte[FILE_SIZE]; for (int i = 0; i < FILE_SIZE; i++) { buffer[i] = 1; } fos.write(buffer); 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"); } }
File created: temp/buffered.dat File size: 10MB Time taken: 7ms
public class ReadFileV4 { public static void main(String[] args) throws IOException { FileInputStream fis = new FileInputStream(FILE_NAME); long startTime = System.currentTimeMillis(); byte[] bytes = fis.readAllBytes(); fis.close(); long endTime = System.currentTimeMillis(); System.out.println("File name: " + FILE_NAME); System.out.println("File size: " + FILE_SIZE / 1024 / 1024 + "MB"); System.out.println("Time taken: " + (endTime - startTime) + "ms"); } }
File name: temp/buffered.dat File size: 10MB Time taken: 2ms
정리
- 파일의 크기가 크지 않아서 메모리 사용에 큰 영향을 주지 않는다면 한번에 처리하기
- 단, 읽기 시 OOM 주의
- 성능이 중요하고 큰 파일을 나누어 처리해야 한다면 버퍼를 직접 다루기
- 성능이 크게 중요하지 않고 버퍼 기능이 필요하다면 BufferedXxx 사용하기
- 동기화로 인한 성능 저하 시 직접 클래스 구현하기
'Java' 카테고리의 다른 글
[Java] record 예약어 (1) 2025.02.13 [Java] 리플렉션(Reflection)에 대해서 (1) 2025.01.17 [Java] Charset과 문자 인코딩 (0) 2024.11.08 [Java] 다형성 (Polymorphism) (0) 2024.11.07 [Java] Dump File에 대해서 (0) 2024.10.31