ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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