I/O 기본 – 스트림과 파일 입출력

스트림(Stream)의 개념

자바에서 외부 데이터와의 입출력은 스트림(Stream)을 통해 이루어진다. 스트림은 데이터의 흐름을 나타내는 추상화된 개념으로 파일, 네트워크, 콘솔 등 다양한 입출력 작업을 일관된 방식으로 처리할 수 있게 해준다

스트림의 방향성

스트림은 단뱡향이다

  • 출력 스트림(OutputStream): 자바 프로세스 → 외부 (데이터 전송)
  • 입력 스트림(InputStream): 외부 → 자바 프로세스 (데이터 수신)

따라서 양방향 통신을 위해서는 입력과 출력 스트림을 각각 생성해야 한다

기본 파일 입출력

프로젝트 설정

중요: 프로젝트 루트 디렉토리에 temp 폴더를 생성해야 한다. src 하위가 아닌 프로젝트 루트에 생성해야 하며, 그렇지 않으면 FileNotFoundException이 발생한다. (자바에서 디렉토리를 생성할 수 있지만 다음에 알아본다)

파일 쓰기와 읽기

public class StreamStartMain1 {
    public static void main(String[] args) throws IOException {
        // 파일에 쓰기
        FileOutputStream fos = new FileOutputStream("temp/hello.dat");
        fos.write(65);  // 'A'
        fos.write(66);  // 'B'
        fos.write(67);  // 'C'
        fos.close();

        // 파일에서 읽기
        FileInputStream fis = new FileInputStream("temp/hello.dat");
        System.out.println(fis.read());  // 65
        System.out.println(fis.read());  // 66
        System.out.println(fis.read());  // 67
        System.out.println(fis.read());  // -1 (EOF)
        fis.close();
    }
}

핵심 개념

  • write(): 바이트 단위로 데이터를 출력
  • read(): 바이트 단위로 데이터를 읽음
  • EOF(End of File): 파일의 끝에 도달하면 -1 반환
  • close(): 외부 자원 사용 후 반드시 닫아야 함 (GC 대상이 아님)
텍스트 에디터에서 ABC로 보이는 이유

파일에는 65, 66, 67이라는 바이트 값이 저장된다. 하지만 텍스트 에디터(UTF-8 등)는 이를 ASCII 코드로 해석하여 A, B, C로 표시한다. 자바에서 read()로 직접 읽으면 원본 바이트 값(65, 66, 67)을 확인할 수 있다

FileOutputStream의 append 옵션

// 기존 내용에 추가
FileOutputStream fos = new FileOutputStream("temp/hello.dat", true);

// 기존 내용 삭제 후 새로 작성 (기본값)
FileOutputStream fos = new FileOutputStream("temp/hello.dat", false);

효율적인 파일 읽기

반복문을 이용한 전체 읽기

FileInputStream fis = new FileInputStream("temp/hello.dat");
int data;
while ((data = fis.read()) != -1) {
    System.out.println(data);
}
fis.close();

EOF(-1)를 만날 때까지 반복하여 파일 전체를 읽을 수 있다

read()가 int를 반환하는 이유

자바의 byte타입은 -128 ~ 127의 범위를 가진다. 하지만

  • 바이트는 0 ~ 255의 256가지 값을 표현해야 한다
  • 추가로 EOF를 나타내는 특수 값(-1)이 필요하다

배열을 이용한 입출력

바이트 배열로 한 번에 쓰기

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);  // 3
System.out.println(Arrays.toString(buffer));     // [65, 66, 67, 0, 0, 0, 0, 0, 0, 0]
fis.close();
파라미터
  • buffer: 데이터를 저장할 바이트 배열
  • offset: 배열의 시작 인덱스
  • length: 읽어올 최대 바이트 수
  • 반환값: 실제로 읽은 바이트 수, EOF면 -1

전체 내용 한 번에 읽기

FileInputStream fis = new FileInputStream("temp/hello.dat");
byte[] readBytes = fis.readAllBytes();
System.out.println(Arrays.toString(readBytes));  // [65, 66, 67]
fis.close();

읽기 방식 비교

read(byte[], offset, length) – 부분 읽기

장점

  • 메모리 사용량 제어 가능
  • 대용량 파일 처리에 적합
  • 예: 100MB 파일을 1MB씩 나누어 처리 → 최대 1MB 메모리만 사용

사용 사례

  • 스트리밍 처리
  • 대용량 파일의 점진적 처리

readAllBytes() – 전체 읽기

장점

  • 간편한 사용
  • 코드 간결성

단점

  • 메모리 사용량 제어 불가
  • 대용량 파일 시 OutOfMemoryError 위험

사용 사례

  • 작은 파일 처리
  • 메모리에 전체 내용을 올려야 하는 경우

InputStream과 OutputStream 추상화

자바는 다양한 입출력 소스(파일, 네트워크, 메모리)를 일관되게 처리하기 위해 InputStreamOutputStream 추상 클래스를 제공한다

InputStream 계층 구조

InputStream
├── FileInputStream      (파일)
├── ByteArrayInputStream (메모리)
└── SocketInputStream    (네트워크)

공통 메서드: read(), read(byte[]), readAllBytes()

OutputStream 계층 구조

OutputStream
├── FileOutputStream      (파일)
├── ByteArrayOutputStream (메모리)
└── SocketOutputStream    (네트워크)

공통 메서드: write(int), write(byte[])

메모리 스트림

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));  // [1, 2, 3]

참고: 실무에서는 컬렉션이나 배열을 주로 사용하며, 메모리 스트림은 테스트나 디버깅 용도로 활용된다

콘솔 스트림

PrintStream printStream = System.out;

// OutputStream 부모 기능
byte[] bytes = "Hello!\n".getBytes(UTF_8);
printStream.write(bytes);

// PrintStream 추가 기능
printStream.println("Print!");

System.out의 정체는 PrintStream이다. PrintStreamOutputStream을 상속받아 바이트 출력 기능을 제공하며, 추가로 문자열을 편리하게 출력하는 println() 등의 메서드를 제공한다

내부 동작: println(String)은 내부적으로 문자열을 바이트 배열로 변환한 후 출력한다

추상화의 장점

  • 일관성: 모든 입출력 작업에 동일한 메서드 사용
  • 유연성: 데이터 소스 변경 시 코드 수정 최소화
  • 확장성: 새로운 스트림 타입 쉽게 추가 가능
  • 재사용성: 다양한 스트림 조합으로 복잡한 작업 구현 (예: BufferedInputStream, DataInputStream)
  • 표준화된 예외 처리: IOException을 통해 일관된 에러 처리

참고: InputStreamOutputStream은 자바 1.0부터 제공된 추상 클래스로, 일부 구현 코드를 포함하고 있어 인터페이스가 아닌 추상 클래스로 설계되어있다

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