Ch 15.2 보조 스트림 (Filter Stream)
보조 스트림은 데이터를 독립적으로 입출력할 수 있는 능력은 없지만, 다른 스트림을 감싸서(Wrapping) 새로운 기능(버퍼링, 데이터 형변환 등)을 추가해주는 스트림입니다. 이를 데코레이터 패턴(Decorator Pattern) 이라고 합니다.
기존 객체를 감싸서 새 기능을 추가하는 패턴. 상속 대신 합성(Composition)을 사용하므로 런타임에 기능을 자유롭게 조합할 수 있습니다.
1. 보조 스트림의 연결 원리
보조 스트림을 사용하려면 먼저 데이터를 직접 주고받는 노드 스트림 을 생성한 뒤, 생성된 노드 스트림을 보조 스트림의 생성자에 전달하여 연결합니다.
import java.io.*;
public class FilterStreamChain {
public static void main(String[] args) throws IOException {
// 1단계: 노드 스트림 (실제 파일과 연결)
FileInputStream fis = new FileInputStream("test.txt");
// 2단계: 보조 스트림으로 감싸기 (버퍼 기능 추가)
BufferedInputStream bis = new BufferedInputStream(fis);
// 3단계: 또 다른 보조 스트림으로 감싸기 (데이터 타입 읽기 기능 추가)
DataInputStream dis = new DataInputStream(bis);
// 읽을 때는 가장 바깥쪽 스트림의 메서드를 사용
// dis → bis → fis 순으로 내부적으로 호출됨
dis.close(); // 닫을 때도 가장 바깥쪽만 닫으면 안쪽까지 자동으로 닫힘
}
}
2. BufferedInputStream / BufferedOutputStream
버퍼링의 원리
디스크는 메모리보다 수천 배 느립니다. 1바이트씩 읽고 쓰면 매번 디스크에 접근하므로 성능이 극도로 저하됩니다. 버퍼 스트림은 메모리에 버퍼(임시 저장소)를 두어 데이터를 모았다가 한 번에 디스크에 접근합니다.
import java.io.*;
public class BufferedStreamExample {
public static void main(String[] args) throws IOException {
String filename = "large_file.txt";
// 파일 생성 (테스트용)
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filename))) {
for (int i = 0; i < 10000; i++) {
bw.write("라인 " + i + ": Hello, BufferedStream!\n");
}
} // 자동으로 flush() 후 close()
// 버퍼 없이 읽기 (느림)
long start1 = System.currentTimeMillis();
try (FileInputStream fis = new FileInputStream(filename)) {
while (fis.read() != -1) {} // 1바이트씩 읽기
}
System.out.println("버퍼 없음: " + (System.currentTimeMillis() - start1) + "ms");
// 버퍼 있이 읽기 (빠름)
long start2 = System.currentTimeMillis();
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filename))) {
while (bis.read() != -1) {} // 내부적으로 8192바이트씩 읽어 캐시
}
System.out.println("버퍼 있음: " + (System.currentTimeMillis() - start2) + "ms");
}
}
flush()의 중요성
버퍼에 데이터가 차지 않으면 디스크에 쓰이지 않을 수 있습니다. flush()는 버퍼의 내용을 강제로 출력합니다.
import java.io.*;
public class FlushExample {
public static void main(String[] args) throws IOException {
// 버퍼 크기를 크게 설정 (기본: 8192 bytes)
try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream("output.txt"), 65536)) { // 64KB 버퍼
bos.write("첫 번째 데이터".getBytes());
// 여기서 프로그램이 종료되면? 버퍼에 남아있어 파일에 안 써질 수 있음!
bos.flush(); // 명시적 flush → 버퍼를 파일에 강제 기록
System.out.println("flush 완료");
bos.write("두 번째 데이터".getBytes());
// close() 호출 시 자동으로 flush() 후 닫힘 → try-with-resources 사용 권장
}
// 확인
System.out.println(new String(java.nio.file.Files.readAllBytes(
java.nio.file.Path.of("output.txt"))));
}
}
네트워크 스트림 등에서는 close()가 안 되는 상황에서도 데이터를 보내야 할 때 반드시 flush()를 명시적으로 호출해야 합니다. 특히 PrintWriter에서 autoFlush가 false인 경우 주의하세요.
커스텀 버퍼 크기 설정
import java.io.*;
public class BufferSizeExample {
public static void main(String[] args) throws IOException {
// 기본 버퍼 크기: 8192 bytes (8KB)
BufferedInputStream defaultBuf = new BufferedInputStream(
new FileInputStream("file.txt")
);
// 커스텀 버퍼 크기: 65536 bytes (64KB) - 대용량 파일에 유리
BufferedInputStream largeBuf = new BufferedInputStream(
new FileInputStream("file.txt"), 65536
);
defaultBuf.close();
largeBuf.close();
}
}
3. DataInputStream / DataOutputStream
기본형 데이터(int, double, boolean 등)와 String을 바이트 스트림으로 직접 읽고 쓸 수 있습니다.
import java.io.*;
public class DataStreamExample {
public static void main(String[] args) throws IOException {
String filename = "data.bin";
// 기본형 데이터 쓰기
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream(filename)))) {
dos.writeBoolean(true); // 1 byte
dos.writeByte(127); // 1 byte
dos.writeChar('A'); // 2 bytes
dos.writeShort(32767); // 2 bytes
dos.writeInt(2_147_483_647); // 4 bytes
dos.writeLong(Long.MAX_VALUE); // 8 bytes
dos.writeFloat(3.14f); // 4 bytes
dos.writeDouble(3.141592653); // 8 bytes
dos.writeUTF("안녕하세요"); // 가변 길이 (UTF-8 인코딩)
}
// 기본형 데이터 읽기 (쓴 순서와 동일하게!)
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream(filename)))) {
System.out.println("boolean: " + dis.readBoolean());
System.out.println("byte: " + dis.readByte());
System.out.println("char: " + dis.readChar());
System.out.println("short: " + dis.readShort());
System.out.println("int: " + dis.readInt());
System.out.println("long: " + dis.readLong());
System.out.println("float: " + dis.readFloat());
System.out.println("double: " + dis.readDouble());
System.out.println("UTF: " + dis.readUTF());
}
}
}
DataOutputStream으로 쓴 순서와 DataInputStream으로 읽는 순서가 반드시 일치해야 합니다. 순서가 다르면 데이터가 깨집니다.
4. PushbackInputStream: 데이터 되돌리기
읽은 데이터를 다시 스트림에 "밀어 넣어서(push back)" 다음 읽기에서 다시 읽을 수 있게 합니다. 렉서(Lexer), 파서(Parser) 구현에 유용합니다.
import java.io.*;
public class PushbackExample {
public static void main(String[] args) throws IOException {
byte[] data = "Hello, World!".getBytes();
ByteArrayInputStream bais = new ByteArrayInputStream(data);
// pushbackSize: 되돌릴 수 있는 최대 바이트 수
try (PushbackInputStream pbis = new PushbackInputStream(bais, 5)) {
// H 읽기
int ch = pbis.read();
System.out.println("읽은 문자: " + (char) ch); // H
// H를 다시 스트림에 밀어 넣기
pbis.unread(ch);
System.out.println("unread 후 다시 읽기: " + (char) pbis.read()); // H (다시)
// 여러 바이트 되돌리기
byte[] lookahead = new byte[5];
pbis.read(lookahead);
System.out.println("미리 읽기: " + new String(lookahead)); // ello,
pbis.unread(lookahead); // 전부 되돌리기
System.out.println("되돌린 후: " + (char) pbis.read()); // e (다시)
}
}
}
5. PrintStream / PrintWriter: 형식화 출력
System.out이 바로 PrintStream입니다. 다양한 타입을 편리하게 출력할 수 있고, 자동 flush 기능도 제공합니다.
import java.io.*;
public class PrintStreamExample {
public static void main(String[] args) throws IOException {
// PrintStream: 파일에 형식화 출력
try (PrintStream ps = new PrintStream(
new BufferedOutputStream(new FileOutputStream("output.txt")), true, "UTF-8")) {
ps.println("첫 번째 줄");
ps.printf("이름: %s, 나이: %d%n", "Alice", 30);
ps.printf("평균: %.2f%n", 95.678);
ps.print("마지막");
}
// PrintWriter: 문자 기반, autoFlush 지원
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("report.txt")), true)) {
pw.println("=== 보고서 ===");
pw.printf("총 매출: %,d원%n", 12_345_678);
pw.printf("평균 단가: %.0f원%n", 12345.678);
}
// 파일 읽어서 출력
try (BufferedReader br = new BufferedReader(new FileReader("output.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
6. InputStreamReader / OutputStreamWriter: 바이트-문자 변환
바이트 스트림을 문자 스트림으로 변환합니다. 인코딩(UTF-8, EUC-KR 등)을 명시적으로 지정할 수 있습니다.
import java.io.*;
import java.nio.charset.StandardCharsets;
public class StreamConverterExample {
public static void main(String[] args) throws IOException {
// System.in(바이트 기반) → InputStreamReader(문자 변환) → BufferedReader(줄 읽기)
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8)
);
System.out.print("입력하세요: ");
// String line = br.readLine(); // 실제 입력 대기 (여기서는 생략)
// 파일을 특정 인코딩으로 읽기
try (BufferedReader fileReader = new BufferedReader(
new InputStreamReader(new FileInputStream("korean.txt"), "UTF-8"))) {
String line;
while ((line = fileReader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("파일 없음 (예제용)");
}
// OutputStreamWriter: 문자를 특정 인코딩으로 바이트로 변환하여 저장
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("output_utf8.txt"), "UTF-8"))) {
bw.write("안녕하세요, UTF-8 파일입니다.");
bw.newLine(); // 줄바꿈
bw.write("Hello, World!");
}
}
}
7. 실전 예제: 텍스트 파일 고속 복사
import java.io.*;
import java.nio.charset.StandardCharsets;
public class FastTextFileCopy {
/**
* BufferedReader + BufferedWriter로 텍스트 파일을 줄 단위로 빠르게 복사
* 각 줄을 가공할 수 있어 단순 바이트 복사보다 유연함
*/
static int copyTextFile(String src, String dst) throws IOException {
int lineCount = 0;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream(src), StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(dst), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine(); // OS에 맞는 줄바꿈 문자 자동 삽입
lineCount++;
}
// try-with-resources 종료 시 자동 flush() + close()
}
return lineCount;
}
/**
* 라인 번호를 붙여서 복사
*/
static void copyWithLineNumbers(String src, String dst) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(src));
PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(dst)))) {
String line;
int lineNum = 1;
while ((line = reader.readLine()) != null) {
writer.printf("%4d | %s%n", lineNum++, line);
}
}
}
public static void main(String[] args) throws IOException {
// 테스트 파일 생성
try (PrintWriter pw = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("source.txt"), "UTF-8")))) {
pw.println("첫 번째 줄: 안녕하세요");
pw.println("두 번째 줄: Hello, World");
pw.println("세 번째 줄: 자바 I/O 스트림");
pw.println("네 번째 줄: BufferedReader & BufferedWriter");
pw.println("다섯 번째 줄: 고성능 파일 처리");
}
// 단순 복사
int lines = copyTextFile("source.txt", "dest.txt");
System.out.println("복사 완료: " + lines + "줄");
// 줄 번호 붙여서 복사
copyWithLineNumbers("source.txt", "dest_numbered.txt");
System.out.println("줄 번호 버전 복사 완료");
// 결과 출력
System.out.println("\n=== 줄 번호 버전 ===");
try (BufferedReader br = new BufferedReader(new FileReader("dest_numbered.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
8. 보조 스트림 정리 표
| 보조 스트림 | 감싸는 대상 | 기능 |
|---|---|---|
BufferedInputStream | InputStream | 버퍼링으로 읽기 성능 향상 |
BufferedOutputStream | OutputStream | 버퍼링으로 쓰기 성능 향상 |
BufferedReader | Reader | 버퍼링 + readLine() |
BufferedWriter | Writer | 버퍼링 + newLine() |
DataInputStream | InputStream | 기본형 타입으로 읽기 |
DataOutputStream | OutputStream | 기본형 타입으로 쓰기 |
InputStreamReader | InputStream | 바이트 → 문자 변환 (인코딩 지정) |
OutputStreamWriter | OutputStream | 문자 → 바이트 변환 (인코딩 지정) |
PushbackInputStream | InputStream | 읽은 데이터 되돌리기 |
PrintStream | OutputStream | 형식화 출력 (printf 등) |
PrintWriter | Writer / OutputStream | 형식화 문자 출력 |
ObjectInputStream | InputStream | 객체 역직렬화 |
ObjectOutputStream | OutputStream | 객체 직렬화 |
- 텍스트 파일 읽기:
BufferedReader(new InputStreamReader(new FileInputStream(...), "UTF-8")) - 텍스트 파일 쓰기:
PrintWriter(new BufferedWriter(new OutputStreamWriter(new FileOutputStream(...), "UTF-8"))) - 바이너리 파일 읽기:
DataInputStream(new BufferedInputStream(new FileInputStream(...))) - 콘솔 입력:
BufferedReader(new InputStreamReader(System.in))