Ch 15.1 자바 입출력 (I/O) 개요
자바에서 입출력(I/O, Input/Output)은 컴퓨터 내부의 메모리와 키보드, 모니터, 파일, 네트워크 등 외부의 장치 간에 데이터를 주고받는 과정을 의미합니다. 자바의 java.io 패키지는 이러한 데이터 입출력을 일관된 방식으로 처리할 수 있는 다양한 클래스를 제공합니다.
I/O 스트림 vs Stream API
자바 8의 Stream API(컬렉션 파이프라인)와 I/O 스트림은 이름이 같지만 전혀 다른 개념입니다. I/O 스트림은 데이터의 흐름(바이트/문자의 연속)을 의미합니다.
1. 스트림(Stream)이란?
I/O에서 스트림이란 데이터가 흐르는 단방향 통로 입니다. 마치 물이 파이프를 통해 한 방향으로만 흐르는 것처럼, 데이터도 입력 스트림 또는 출력 스트림으로 구분되어 한 방향으로만 흐릅니다.
- 입력 스트림(Input Stream): 외부 → 프로그램 (데이터를 읽어들임)
- 출력 스트림(Output Stream): 프로그램 → 외부 (데이터를 내보냄)
2. 스트림의 두 가지 분류
2.1 데이터 처리 단위에 따른 분류
| 분류 | 최상위 추상 클래스 | 처리 단위 | 용도 |
|---|---|---|---|
| 바이트 기반 | InputStream / OutputStream | 1 byte | 이미지, 동영상, 모든 파일 |
| 문자 기반 | Reader / Writer | 2 byte (char) | 텍스트 전용 |
2.2 방향에 따른 분류
| 방향 | 바이트 기반 | 문자 기반 |
|---|---|---|
| 입력 | InputStream | Reader |
| 출력 | OutputStream | Writer |
3. 바이트 기반 스트림 계층 구조
1 byte 단위로 데이터를 주고받습니다. 이미지, 동영상, 텍스트 등 모든 종류의 데이터 를 전송할 수 있습니다.
InputStream (추상 클래스)
├── FileInputStream 파일에서 바이트 읽기
├── ByteArrayInputStream byte[] 배열에서 읽기
├── FilterInputStream 보조 스트림 기반 (데코레이터)
│ ├── BufferedInputStream 버퍼링
│ ├── DataInputStream 기본형 타입으로 읽기
│ └── PushbackInputStream 읽은 데이터 되돌리기
└── ObjectInputStream 객체 역직렬화
OutputStream (추상 클래스)
├── FileOutputStream 파일에 바이트 쓰기
├── ByteArrayOutputStream byte[] 배열에 쓰기
├── FilterOutputStream 보조 스트림 기반 (데코레이터)
│ ├── BufferedOutputStream 버퍼링
│ ├── DataOutputStream 기본형 타입으로 쓰기
│ └── PrintStream 형식화 출력 (System.out)
└── ObjectOutputStream 객체 직렬화
4. 문자 기반 스트림 계층 구조
자바의 char는 2 byte이기 때문에 1 byte 기반의 스트림으로 문자를 처리하면 한글 등이 깨질 수 있습니다. 오로지 문자 데이터(텍스트) 만을 처리하기 위해 만들어진 스트림입니다.
Reader (추상 클래스)
├── FileReader 파일에서 문자 읽기
├── StringReader 문자열에서 읽기
├── CharArrayReader char[] 배열에서 읽기
├── InputStreamReader 바이트 스트림 → 문자 스트림 변환
└── BufferedReader 버퍼링 + readLine()
Writer (추상 클래스)
├── FileWriter 파일에 문자 쓰기
├── StringWriter 문자열로 쓰기
├── CharArrayWriter char[] 배열에 쓰기
├── OutputStreamWriter 문자 스트림 → 바이트 스트림 변환
├── BufferedWriter 버퍼링
└── PrintWriter 형식화 출력
5. InputStream / OutputStream 핵심 메서드
InputStream 주요 메서드
import java.io.*;
public class InputStreamMethods {
public static void main(String[] args) throws IOException {
byte[] data = {65, 66, 67, 68, 69}; // A B C D E
InputStream is = new ByteArrayInputStream(data);
// read(): 1바이트 읽기. 끝이면 -1 반환
int b = is.read();
System.out.println((char) b); // A
// read(byte[] buf): buf에 읽어 넣고 읽은 바이트 수 반환
byte[] buf = new byte[3];
int bytesRead = is.read(buf);
System.out.println(bytesRead); // 3
System.out.println(new String(buf)); // BCD
// available(): 블로킹 없이 읽을 수 있는 바이트 수
System.out.println(is.available()); // 1 (E 남음)
// skip(n): n바이트 건너뛰기
is.skip(1);
System.out.println(is.read()); // -1 (끝)
is.close();
}
}
OutputStream 주요 메서드
import java.io.*;
public class OutputStreamMethods {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// write(int b): 1바이트 쓰기
baos.write(65); // 'A'
// write(byte[] b): 바이트 배열 전체 쓰기
baos.write(new byte[]{66, 67, 68}); // B C D
// write(byte[] b, offset, length): 일부만 쓰기
byte[] data = {69, 70, 71, 72}; // E F G H
baos.write(data, 0, 2); // E F만
// flush(): 버퍼에 남은 데이터 강제 출력
baos.flush();
System.out.println(baos.toString()); // ABCDEF
baos.close();
}
}
6. Reader / Writer 핵심 메서드
import java.io.*;
public class ReaderWriterMethods {
public static void main(String[] args) throws IOException {
// StringReader: 문자열을 스트림처럼 읽기
String text = "Hello, 안녕하세요!";
Reader reader = new StringReader(text);
// read(): 문자 1개 읽기
int c = reader.read();
System.out.println((char) c); // H
// read(char[] buf): char 배열에 읽기
char[] buf = new char[5];
reader.read(buf);
System.out.println(new String(buf)); // ello,
reader.close();
// StringWriter: 출력을 문자열로 캡처
StringWriter sw = new StringWriter();
sw.write("Hello");
sw.write(", World!");
System.out.println(sw.toString()); // Hello, World!
sw.close();
}
}
7. 데코레이터 패턴으로 스트림 기능 확장
자바 I/O는 데코레이터 패턴을 사용하여 기본 스트림에 기능을 레이어처럼 추가합니다.
import java.io.*;
public class DecoratorPattern {
public static void main(String[] args) throws IOException {
// 파일 스트림 (기본 노드 스트림)
// FileInputStream → BufferedInputStream → DataInputStream
// 각 단계마다 기능이 추가됨
// 쓰기: File → Buffered → Data (기본형 데이터 쓰기)
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("data.bin")))) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeUTF("Hello");
}
// 읽기: File → Buffered → Data (기본형 데이터 읽기)
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.bin")))) {
System.out.println(dis.readInt()); // 42
System.out.println(dis.readDouble()); // 3.14
System.out.println(dis.readUTF()); // Hello
}
}
}
8. try-with-resources로 자동 자원 해제
I/O 스트림은 시스템 자원을 사용하므로 반드시 사용 후 close()해야 합니다. AutoCloseable 인터페이스를 구현한 스트림은 try-with-resources로 자동 해제됩니다.
import java.io.*;
public class TryWithResources {
public static void main(String[] args) {
// 구식 방법: 직접 close() (실수하기 쉬움)
FileInputStream fis = null;
try {
fis = new FileInputStream("test.txt");
// ... 작업
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 권장 방법: try-with-resources (Java 7+)
// try 블록 종료 시 자동으로 close() 호출
try (FileInputStream fis2 = new FileInputStream("test.txt")) {
// ... 작업
} catch (IOException e) {
// 파일이 없을 경우 무시 (예제용)
}
// 여러 스트림을 함께 사용
try (
FileInputStream input = new FileInputStream("source.txt");
FileOutputStream output = new FileOutputStream("dest.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
9. 실전 예제: 바이트 배열로 읽고 쓰기
import java.io.*;
public class ByteArrayIOExample {
public static void main(String[] args) throws IOException {
// 1. ByteArrayOutputStream: 메모리에 바이트 데이터 쌓기
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeInt(2024);
dos.writeDouble(3.14159);
dos.writeUTF("자바 I/O 테스트");
dos.flush();
byte[] bytes = baos.toByteArray();
System.out.println("직렬화된 바이트 수: " + bytes.length);
// 2. ByteArrayInputStream: 바이트 배열을 스트림처럼 읽기
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
DataInputStream dis = new DataInputStream(bais);
System.out.println("연도: " + dis.readInt()); // 2024
System.out.println("PI: " + dis.readDouble()); // 3.14159
System.out.println("메시지: " + dis.readUTF()); // 자바 I/O 테스트
dos.close();
dis.close();
}
}
10. 실전 예제: 간단한 파일 복사
import java.io.*;
public class FileCopy {
// 방법 1: 1바이트씩 읽기 (느림)
static void copyByteByte(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
int b;
while ((b = in.read()) != -1) {
out.write(b);
}
}
}
// 방법 2: 버퍼 배열로 읽기 (빠름)
static void copyWithBuffer(String src, String dst) throws IOException {
try (InputStream in = new BufferedInputStream(new FileInputStream(src));
OutputStream out = new BufferedOutputStream(new FileOutputStream(dst))) {
byte[] buffer = new byte[8192]; // 8KB 버퍼
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
// 방법 3: Java NIO (가장 현대적)
static void copyWithNio(String src, String dst) throws IOException {
java.nio.file.Files.copy(
java.nio.file.Path.of(src),
java.nio.file.Path.of(dst),
java.nio.file.StandardCopyOption.REPLACE_EXISTING
);
}
public static void main(String[] args) throws IOException {
// 테스트용 파일 생성
try (FileWriter fw = new FileWriter("original.txt")) {
fw.write("Hello, Java I/O!\n이것은 파일 복사 테스트입니다.");
}
// 복사 실행
copyWithBuffer("original.txt", "copied.txt");
System.out.println("파일 복사 완료!");
// 복사본 읽기 확인
try (BufferedReader br = new BufferedReader(new FileReader("copied.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
}
파일 복사 성능 비교
- 1바이트씩 복사: 매번 OS 시스템 콜 발생 → 매우 느림
- 버퍼 배열(8KB): 8192개 바이트를 한 번에 읽고 씀 → 수십~수백 배 빠름
- Java NIO
Files.copy(): 네이티브 파일 복사 사용 → 가장 빠름
11. java.io vs java.nio 비교
| 항목 | java.io | java.nio |
|---|---|---|
| 방식 | 스트림 기반 (단방향) | 채널/버퍼 기반 (양방향) |
| 블로킹 | 블로킹 I/O | 논블로킹 가능 |
| 데이터 단위 | 바이트/문자 단위 | 버퍼(Buffer) 단위 |
| 사용 편의성 | 간단하고 직관적 | 코드가 복잡하나 성능 우수 |
| 권장 상황 | 간단한 파일 I/O | 고성능 서버, 대용량 파일 |
| 주요 클래스 | FileInputStream | FileChannel, ByteBuffer |