스트림(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 추상화
자바는 다양한 입출력 소스(파일, 네트워크, 메모리)를 일관되게 처리하기 위해 InputStream과 OutputStream 추상 클래스를 제공한다
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이다. PrintStream은 OutputStream을 상속받아 바이트 출력 기능을 제공하며, 추가로 문자열을 편리하게 출력하는 println() 등의 메서드를 제공한다
내부 동작: println(String)은 내부적으로 문자열을 바이트 배열로 변환한 후 출력한다
추상화의 장점
- 일관성: 모든 입출력 작업에 동일한 메서드 사용
- 유연성: 데이터 소스 변경 시 코드 수정 최소화
- 확장성: 새로운 스트림 타입 쉽게 추가 가능
- 재사용성: 다양한 스트림 조합으로 복잡한 작업 구현 (예:
BufferedInputStream,DataInputStream) - 표준화된 예외 처리:
IOException을 통해 일관된 에러 처리
참고: InputStream과 OutputStream은 자바 1.0부터 제공된 추상 클래스로, 일부 구현 코드를 포함하고 있어 인터페이스가 아닌 추상 클래스로 설계되어있다