I/O 기본

수호천사임다·2024년 10월 13일

자바

목록 보기
7/15

출력 스트림 - 자바 프로스세가 가지고 있는 데이터를 밖으로 보내는 것.
입력 스트림 - 반대로 외부 데이터를 자바 프로세스 안으로 가져오는 것.

자바에서는 스트림을 통해서 바이트로 데이터를 외부로 주고받는다.
(각 스트림은 단방향으로 흐름)

FileOutputStream, FileInputStream

  • 파일에 데이터를 바이트 단위로 기록, 읽을때 사용하는 클래스
  • 주로 텍스트나 이진 데이터를 저장 및 읽는데 사용
    | 메서드/클래스 | 설명 |
    |---------------------------|--------------------------------------------------------------------------------------------------------|
    | new FileOutputStream() | - 파일에 데이터를 출력하는 스트림.
    - 파일이 없으면 파일을 자동으로 만들고, 데이터를 해당 파일에 저장한다.
    - 폴더는 생성하지 않는다. |
    | write() | - byte 단위로 값을 출력한다. |
    | new FileInputStream() | - 파일에서 데이터를 읽어오는 스트림. |
    | read() | - 파일에서 데이터를 byte 단위로 하나씩 읽어온다.
    - 파일의 끝에 도달하면 -1을 반환한다. (EOF: End of File) |
    | close() | 파일에 접근하는 것은 자바 입장에서 외부 자원을 사용하는 것이다.
    내부 객체는 자동으로 GC가 되지만, 외부 자원은 사용 후 반드시 닫아야 한다. |

간단한 실습

package io.start;

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

public class StreamStartMain2 {

  public static void main(String[] args) throws IOException {
    FileOutputStream fos = new FileOutputStream("temp/hello.dat");
    // 파일에 뭔가 저장을 하려면 FileOutputStream이 필요
    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);
    }
    fis.close();
  }
}
  • while((data = fis.read()) != -1 를 사용하면 파일의 끝까지 출력할 수 있다.

byte[] 을 사용해서 데이터를 원하는 크기 만큼 더 편리하게 저장하고 읽는 방법

package io.start;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class StreamStartMain3 {

  public static void main(String[] args) throws IOException {
    FileOutputStream fos = new FileOutputStream("temp/hello.dat");
    byte[] input = {65, 66, 67, 68};
    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();
  }
}
  • write(byte[]) : byte[]에 원하는 데이터를 담고 write()에 전달하면 해당 데이터를 한 번에 출력

  • read(byte[], offset, length): byte[]을 미리 만들어두고, 만들어둔 byte[]에 한 번에 데이터를 읽어올 수 있다.

  • byte[]: 데이터가 읽혀지는 버퍼

  • offset: 데이터 기록되는 byte[] 인덱스 시작 위치

  • length: 읽어올 byte의 최대 길이

  • 반환 값: 버퍼에 읽은 총 바이트 수 여기서는 3byte를 읽었으므로 3이 반한된다.

  FileInputStream fis = new FileInputStream("temp/hello.dat");
    byte[] readbytes = fis.readAllBytes();
    System.out.println(Arrays.toString(readbytes));
    fis.close();
  • readAllBytes(): 스트림이 끝날 때 까지 모든 데이터를 한 번에 읽어올 수 있다.

부분으로 나누어 읽기 vs 전체 읽기

  • `read(byte[], offset, length)
    • 스트림의 내용을 부분적으로 읽거나, 읽은 내용을 처리하면서 스트림을 계속해서 읽어야 할 경우에 적합
    • 메모리 사용량을 제어 가능
    • ex) 파일이나 스트림에서 일정한 크기의 데이터를 반복적으로 읽어야 할 때 유용, 대용량 파일을처리할 때, 한 번에 메모리에 로드하기보다는 이 메서드를 사용하여 파일을 조각조각 읽어들일 수 있다.
  • readAllBytes()
    • 한 번의 호출로 모든 데이터를 읽을 수 있어 편리
    • 작은 파일이나 메모리에 모든 내용을 올려서 처리해야 하는 경우에 적합
    • 메모리 사용량을 제어할 수 없다.
    • 큰 파일의 경우 OutOfMemoryError가 발생할 수 있다.

InputStream, OutputStream

  • 자바 내부에 있는 데이터를 외부에 있는 파일에 저장하거나, 네트워크를 통해 전송하거나 콘솔에 출력할 때 모두 byte 단위로 데이터를 주고 받는다.

  • ex) 파일, 네트워크, 콘솔 각각 데이터를 주고 받는 방식이 다르다면 상당히 불편, 또한 파일에 저장하던 내용을 네트워크에 전달하거나 콘솔에 출력하도록 변경할 때 너무 많은 코드를 변경해야 할 수 도 있다.

    • 이런 문제를 해결 하길 위해 InputStream, OutputStream이라는 기본 추상 클래스 제공

  • InputStream 과 상속 클래스

  • read() , read(byte[]) , readAllBytes() 제공

  • OutputStream과 상속 클래스
  • write(int) , write(byte[]) 제공

메모리 스트림

package io.start;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class ByteArrayStreamMain {

  public static void main(String[] args) throws IOException {
    byte[] input =  {1, 2, 3};

    // 메모리에 쓰기
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    bos.write(input);

    // 메모리에서 읽기
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    byte[] bytes = bis.readAllBytes();
    System.out.println(Arrays.toString(bytes));
  }
}

`package io.start;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class ByteArrayStreamMain {

  public static void main(String[] args) throws IOException {
    byte[] input =  {1, 2, 3};

    // 메모리에 쓰기
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    bos.write(input);

    // 메모리에서 읽기
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
    byte[] bytes = bis.readAllBytes();
    System.out.println(Arrays.toString(bytes));
  }
}
  • 이 기능은 잘 사용하지 않는다.
  • 주로 스트림을 간단하게 테스트 하거나 스트림의 데이터를 확인하는 용도로 사용

콘솔 스트림

package io.start;

import java.io.IOException;
import java.io.PrintStream;

import static java.nio.charset.StandardCharsets.UTF_8;

public class PrintStreamMain {
  public static void main(String[] args) throws IOException {
    PrintStream printStream = System.out;
    
    byte[] bytes = "Hello!\n".getBytes(UTF_8);
    printStream.write(bytes);
    printStream.println("Print!");
  }
}

우리가 자주 사용했던 System.out은 사실 PrintStream -> OutputStream를 상속받는다.
이 스트림은 자바가 시작될 때 자동으로 만들어진다.

장점설명
일관성모든 종류의 입출력 작업에 대해 동일한 인터페이스(부모의 메서드)를 사용할 수 있어 코드의 일관성이 유지된다.
유연성실제 데이터 소스나 목적지가 무엇인지에 관계없이 동일한 방식으로 코드를 작성할 수 있다. 예를 들어, 파일, 네트워크, 메모리 등 다양한 소스에 대해 동일한 메서드를 사용할 수 있다.
확장성새로운 유형의 입출력 스트림을 쉽게 추가할 수 있다.
재사용성다양한 스트림 클래스를 조합하여 복잡한 입출력 작업을 수행할 수 있다. 예를 들어, BufferedInputStream을 사용하여 성능을 향상시키거나, DataInputStream을 사용하여 기본 데이터 타입을 쉽게 읽을 수 있다.
에러 처리표준화된 예외 처리 메커니즘을 통해 일관된 방식으로 오류를 처리할 수 있다.

1바이트씩 데이터를 전송하는 게 아니라
바이트 배열에 담아서 한번에 여러 바이트 전달

buffer - 데이터를 모아서 전달하거나 모아서 전달받는 용도로 사용되는 것을 버퍼라 함

FileOutputStream 의 write() 를 사용해서 1byte씩 파일을 저장해보자

BufferedOutputStream은 버퍼 기능을 내부에서 대신 처리
단순한 코드를 유지하면서 버퍼를 사용하는 이점도 함께 누릴 수 있음

기본 스트림, 보조 스트림

  • FileOutputStream과 같이 단독으로 사용할 수 있는 스트림을 기본 스트림이라 한다.
  • BufferedOutputStream과 같이 단독으로 사용할 수 없고, 보조 기능을 제공하는 스트림을 보조 스트림이라 한다.
BufferedOutputStream(OutPutStream out, int size) {...}
  • BufferedOutputStream은 버퍼라는 보조 기능을 제공. 누구에게 보조 기능을 제공할지 대상을 반드시 전달해야 한다.

파일 입출력과 성능 최적화

FileOutputStreamwrite()을 사용해서 1byte씩 10MB 파일을 저장하면 상당히 오랜 시간이 걸린다. 마찬가지로 read()를 사용해서 1byte씩 10MB 데이터를 읽어도 상당히 오랜 시간이 걸린다.
이렇게 오래 걸린 이유는 자바에서 1byte씩 디스크에 데이터를 전달하기 때문이다. 디스크는 1byte의 데이터를 받아서 1byte 데이터를 쓴다.

  • write(), read()를 호출할 때마다 OS의 시스템 콜을 통해 파일을 읽거나 쓰는 명령어를 전달(시스템 콜은 상대적으로 무거운 작업)
  • HDD, SDD 같은 장치들도 하나의 데이터를 읽고 쓸 때마다 필요한 시간이 있다.
  • 해결책: 자주 발생하는 시스템 콜로 인한 성능은 피할 수 없다, 자바에서 read(), write() 호출 횟수를 줄여서 시스템콜 횟수도 줄여야 한다.

1byte씩 데이터를 하나씩 전달하는 것이 아니라 byte[]을 통해 배열에 담아서 한 번에 여러 byte를 전달하면 성능을 최적화 할 수 있다.
이렇게 하면 시스템 콜도 줄어들고, HDD, SDD 같은 장치들의 작동 횟수도 줄어든다.

  • ex) 버퍼의 크기를 1->2로 변경하면 시스템 콜 횟수도 절반으로 줄어든다.
  • 하지만 버퍼의 크기가 커진다고 해서 속도가 계속 줄어들지는 않는다.
  • 디스크나 파일 시스템에서 데이터를 읽고 쓰는 기본 단위가 보통 4KB(4096byte) 또는 8KB(8192byte)이기 때문이다.

버퍼 - 데이터를 모아서 전달하거나 모아서 전달받는 용도로 사용하는 것

Buffered

쓰기

  • BufferedOutputStream은 내부에서 단순히 버퍼 기능만 제공
  • 내부에 byte[] buf라는 버퍼를 가지고 있다.
  • BufferedOutputStreamwrite(byte) 를통해 byte 하나를 전달하면 byte[] buf에 보관되며 버퍼의 크기가 차면 보조 기능을 제공받은 Stream에 있는 write(byte[]) 메서드를 호출한다.
  • 누구에게 보조 기능을 제공할지 대상을 반드시 생성자로 전달
  • 추가로 사용할 버퍼의 크기도 함께 전달 가능
package io.buffered;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import static io.buffered.BufferedConst.*;

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");
  }
}

읽기

package io.buffered;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

import static io.buffered.BufferedConst.BUFFER_SIZE;
import static io.buffered.BufferedConst.FILE_NAME;

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 : " + fileSize / 1024 / 1024 + "MB");
    System.out.println("Time taken : " + (endTime - startTime) + "ms");
  }
}
  • read()는 1byte로조회 (버퍼의 크기 3이라고 가정)
  • BuffredInputStream은 먼저 버퍼를 확인 후, 버퍼에 데이터가 없으므로 데이터를 불러온다.
  • BuffredInputStreamFileInputStream에서 read(byte[])를 사용해서 버퍼의 크기인 3byte의 데이터를 불러온다.
  • 불러온 데이터를 버퍼에 보관
  • read()를 호출하면 버퍼에 있는 데이터 중에 1byte를 반환, 버퍼가 비어있으면 FileInputStream에서 버퍼 크기만큼 조회하고 버퍼에 담아둔다.
  • 버퍼에 있는 데이터를 하나 반환하고 계속 반복한다.

BuffredInputStream은 버퍼의 크기만큼 데이터를 미리 읽어서 버퍼에 보관한다. 따라서 read()를 통해 1byte씩 데이터를 조회해도, 성능이 최적화 된다.

버퍼를 직접 다루는 것보다 BufferedXxx 성능이 떨어지는 이유

= BufferedXxx 클래스는 자바초창기에 만들어진 클래스로 , 멀티 스레드를 고려해서 만든 클래스

  • 락을 걸고 푸는 동기화 코드로 인해 성능이 약간 저하될 수 있다.

매우 큰 데이터를 다루어야 하고, 성능 최적화가 중요하다면 직접 버퍼를 다루는 방법도 고려해야한다.

정리

  • 파일의 크기가 크지 않아서, 메모리 사용에 큰 영향을 주지 않는다면 쉽고 빠르게 한 번에 처리
  • 성능이 중요하고 큰 파일을 나누어 처리해야 한다면, 버퍼를 직접 다루기
  • 성능이 크게 중요하지 않고, 버퍼 기능이 필요하면 BufferedXxx를 사용
    • 동기화 코드가 들어있어서 스레드에 안전하지만, 약간의 성능 저하가 있다.

강의자료 출처: 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션

0개의 댓글