Chapter 15. 입출력 (I/O)
입출력 (I/O)
자바의 입출력 스트림 구조, 바이트/문자 기반 스트림, 보조 스트림, 표준 입출력, File 클래스, 직렬화를 다룬다.
1. 자바에서의 입출력
1.1 입출력이란?
I/O(Input/Output)란 컴퓨터 내부 또는 외부 장치와 프로그램 간에 데이터를 주고받는 것을 말한다.
1.2 스트림 (Stream)
스트림이란 데이터를 운반하는 데 사용되는 연결 통로이다.
┌──────────┐ InputStream ┌──────────┐
│ │ ◀───────────────── │ │
│ 프로그램 │ │ 입출력 대상 │
│ │ ──────────────────▶│ │
└──────────┘ OutputStream └──────────┘| 특성 | 설명 |
|---|---|
| 단방향 | 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없다 |
| FIFO | 먼저 보낸 데이터를 먼저 받는다 (큐와 유사) |
| 연속적 | 중간에 건너뜀 없이 연속적으로 데이터를 주고받는다 |
입출력을 동시에 수행하려면 입력 스트림 + 출력 스트림, 총 2개의 스트림이 필요하다.
1.3 스트림의 분류
스트림
┌──────────┴──────────┐
바이트 기반 문자 기반
(1 byte 단위) (2 byte 단위)
┌──────┴──────┐ ┌──────┴──────┐
InputStream OutputStream Reader Writer2. 바이트 기반 스트림
2.1 InputStream과 OutputStream
모든 바이트 기반 스트림의 조상 클래스이다.
| 입력 스트림 | 출력 스트림 | 입출력 대상 |
|---|---|---|
FileInputStream | FileOutputStream | 파일 |
ByteArrayInputStream | ByteArrayOutputStream | 메모리 (byte 배열) |
PipedInputStream | PipedOutputStream | 프로세스 간 통신 |
AudioInputStream | AudioOutputStream | 오디오 장치 |
InputStream의 주요 메서드
| 메서드 | 설명 |
|---|---|
abstract int read() | 1 byte를 읽어서 반환. 읽을 데이터가 없으면 -1 반환 |
int read(byte[] b) | 배열 b의 크기만큼 읽어서 배열에 저장 |
int read(byte[] b, int off, int len) | 최대 len개의 byte를 읽어서 b의 off 위치부터 저장 |
void close() | 스트림을 닫고 자원을 반환 |
int available() | 블로킹 없이 읽어올 수 있는 바이트 수 반환 |
long skip(long n) | n만큼 건너뛴다 |
void mark(int readlimit) | 현재 위치를 표시 |
void reset() | mark()가 호출된 위치로 되돌린다 |
OutputStream의 주요 메서드
| 메서드 | 설명 |
|---|---|
abstract void write(int b) | 1 byte를 출력 |
void write(byte[] b) | 배열 b의 모든 내용을 출력 |
void write(byte[] b, int off, int len) | b의 off부터 len개만큼 출력 |
void flush() | 버퍼의 내용을 출력소스에 쓴다 |
void close() | 스트림을 닫고 자원을 반환 |
// read()의 내부 구조 — 추상 메서드를 활용한 템플릿 메서드 패턴
public abstract class InputStream {
abstract int read(); // 자손 클래스가 구현
int read(byte[] b, int off, int len) {
for (int i = off; i < off + len; i++) {
b[i] = (byte) read(); // 추상 메서드 호출
}
}
int read(byte[] b) {
return read(b, 0, b.length); // 위 메서드 재활용
}
}close()로 자원을 반환해야 한다. JVM이 자동으로 닫아주기는 하지만 이에 의존해서는 안 된다. ByteArrayInputStream이나 System.in, System.out처럼 메모리를 사용하는 스트림은 예외적으로 닫지 않아도 된다.2.2 FileInputStream과 FileOutputStream
파일 입출력에 가장 많이 사용되는 스트림이다.
// 파일 읽기
try (FileInputStream fis = new FileInputStream("input.txt")) {
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}// 파일 복사
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("copy.txt")) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}FileOutputStream 생성자
| 생성자 | 설명 |
|---|---|
FileOutputStream(String name) | 파일에 연결된 출력 스트림 생성 (기존 내용 덮어쓰기) |
FileOutputStream(String name, boolean append) | append가 true면 기존 내용 뒤에 이어쓰기 |
FileOutputStream(File file) | File 인스턴스로 지정 |
// 파일에 이어쓰기
try (FileOutputStream fos = new FileOutputStream("log.txt", true)) {
String msg = "새로운 로그 추가\n";
fos.write(msg.getBytes());
}2.3 ByteArrayInputStream과 ByteArrayOutputStream
메모리(byte 배열)에 데이터를 입출력한다. 다른 곳에 입출력하기 전에 데이터를 임시로 변환할 때 사용한다. 메모리만 사용하므로 GC가 자동으로 자원을 반환하여 close()가 필수가 아니다.
byte[] inSrc = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] outSrc = null;
ByteArrayInputStream input = new ByteArrayInputStream(inSrc);
ByteArrayOutputStream output = new ByteArrayOutputStream();
int data;
while ((data = input.read()) != -1) {
output.write(data);
}
outSrc = output.toByteArray();
System.out.println(Arrays.toString(outSrc));
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]available()은 블로킹 없이 읽어올 수 있는 바이트 수를 반환하므로, 효율적인 배열 크기 설정에 활용할 수 있다.3. 바이트 기반 보조 스트림
보조 스트림은 실제 데이터를 주고받는 기능은 없고, 기반 스트림의 기능을 향상시키거나 새로운 기능을 추가한다. 단독으로 사용할 수 없으며 기반 스트림을 먼저 생성한 후 이를 감싸서 사용한다.
// 기반 스트림 생성
FileInputStream fis = new FileInputStream("file.txt");
// 보조 스트림으로 감싸기
BufferedInputStream bis = new BufferedInputStream(fis);
// 보조 스트림을 통해 읽기
bis.read();3.1 FilterInputStream과 FilterOutputStream
모든 보조 스트림의 조상 클래스이다. 자체적으로는 아무 일도 하지 않으며, 상속을 통해 오버라이딩하여 사용한다.
FilterInputStream의 자손:
├── BufferedInputStream
├── DataInputStream
└── PushbackInputStream
FilterOutputStream의 자손:
├── BufferedOutputStream
├── DataOutputStream
└── PrintStream3.2 BufferedInputStream과 BufferedOutputStream
버퍼를 이용하여 입출력 효율을 높인다. 한 바이트씩 읽고 쓰는 대신 버퍼에 모아서 한 번에 처리한다.
[프로그램] ←→ [BufferedInputStream 버퍼(8KB)] ←→ [파일]
1. 프로그램이 read() 호출
2. 버퍼가 비어있으면 파일에서 8KB를 한 번에 읽어 버퍼에 저장
3. 버퍼에서 1 byte를 반환
4. 버퍼가 비면 다시 파일에서 8KB 읽기 반복// 버퍼 있는 스트림 vs 없는 스트림 — 성능 차이
// 버퍼 없이 파일 복사 (느림)
try (FileInputStream fis = new FileInputStream("large.dat");
FileOutputStream fos = new FileOutputStream("copy.dat")) {
int b;
while ((b = fis.read()) != -1) fos.write(b);
}
// 버퍼로 감싸서 파일 복사 (빠름)
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("large.dat"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.dat"))) {
int b;
while ((b = bis.read()) != -1) bos.write(b);
}| 항목 | 기본 버퍼 크기 | 설명 |
|---|---|---|
BufferedInputStream | 8192 byte (8KB) | 입력 시 버퍼 크기만큼 미리 읽어옴 |
BufferedOutputStream | 8192 byte (8KB) | 버퍼가 가득 차면 한 번에 출력 |
close() 또는 flush()를 호출하여 버퍼의 잔여 내용을 출력해야 한다.3.3 DataInputStream과 DataOutputStream
기본 자료형 단위로 데이터를 읽고 쓸 수 있다. byte 단위가 아닌 int, float, String 등의 단위로 처리한다.
// 기본 자료형 단위로 파일에 쓰기
try (DataOutputStream dos = new DataOutputStream(
new FileOutputStream("data.bin"))) {
dos.writeInt(100);
dos.writeFloat(3.14f);
dos.writeBoolean(true);
dos.writeUTF("Hello");
}
// 기본 자료형 단위로 파일에서 읽기 (쓴 순서대로 읽어야 함)
try (DataInputStream dis = new DataInputStream(
new FileInputStream("data.bin"))) {
int i = dis.readInt(); // 100
float f = dis.readFloat(); // 3.14
boolean b = dis.readBoolean(); // true
String s = dis.readUTF(); // "Hello"
}EOFException이 발생한다.3.4 PrintStream
print, println, printf 메서드를 제공한다. System.out이 바로 PrintStream이다.
printf 주요 포맷
| 포맷 | 설명 | 예시 (int i = 65) |
|---|---|---|
%d | 10진 정수 | 65 |
%o | 8진 정수 | 101 |
%x | 16진 정수 | 41 |
%c | 문자 | A |
%s | 문자열 | 65 |
%f | 실수 | 65.000000 |
%e | 지수 표현 | 6.500000e+01 |
%5d | 5자리, 빈자리 공백 | 65 |
%-5d | 5자리, 왼쪽 정렬 | 65 |
%05d | 5자리, 빈자리 0 | 00065 |
%8.2f | 전체 8자리, 소수 2자리 | 65.00 |
int age = 25;
String name = "홍길동";
double score = 95.7;
System.out.printf("이름: %s, 나이: %d, 점수: %.1f%n", name, age, score);
// 이름: 홍길동, 나이: 25, 점수: 95.74. 문자 기반 스트림
Java의 char는 2 byte이므로 바이트 기반 스트림으로 문자를 처리하기 어렵다. 문자 기반 스트림은 인코딩 변환도 자동으로 처리한다.
4.1 바이트 기반 → 문자 기반 대응
| 바이트 기반 | 문자 기반 |
|---|---|
FileInputStream / FileOutputStream | FileReader / FileWriter |
ByteArrayInputStream / ByteArrayOutputStream | CharArrayReader / CharArrayWriter |
PipedInputStream / PipedOutputStream | PipedReader / PipedWriter |
StringBufferInputStream (deprecated) | StringReader / StringWriter |
보조 스트림 대응
| 바이트 기반 보조 | 문자 기반 보조 |
|---|---|
BufferedInputStream / BufferedOutputStream | BufferedReader / BufferedWriter |
FilterInputStream / FilterOutputStream | FilterReader / FilterWriter |
PrintStream | PrintWriter |
PushbackInputStream | PushbackReader |
4.2 Reader와 Writer
문자 기반 스트림의 조상 클래스이다. byte[] 대신 char[]를 사용한다.
| Reader 메서드 | Writer 메서드 |
|---|---|
int read() | void write(int c) |
int read(char[] c) | void write(char[] c) |
int read(char[] c, int off, int len) | void write(char[] c, int off, int len) |
void close() | void write(String str) |
void flush() / void close() |
4.3 FileReader와 FileWriter
텍스트 파일을 읽고 쓸 때 사용한다.
// 텍스트 파일 읽기
try (FileReader fr = new FileReader("input.txt")) {
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char) ch);
}
}
// 텍스트 파일 쓰기
try (FileWriter fw = new FileWriter("output.txt")) {
fw.write("안녕하세요\n");
fw.write("Java I/O 학습 중입니다.");
}4.4 PipedReader와 PipedWriter
쓰레드 간 데이터 통신에 사용된다. 입력과 출력 스트림을 connect()로 연결한다. 한쪽 스트림만 닫아도 나머지가 자동으로 닫힌다.
4.5 StringReader와 StringWriter
입출력 대상이 메모리(문자열)인 스트림이다. StringWriter는 내부에 StringBuffer를 가지고 있다.
StringWriter sw = new StringWriter();
sw.write("Hello ");
sw.write("World");
String result = sw.toString(); // "Hello World"
StringBuffer buf = sw.getBuffer(); // StringBuffer 직접 접근5. 문자 기반 보조 스트림
5.1 BufferedReader와 BufferedWriter
버퍼를 이용하여 문자 기반 입출력의 효율을 높인다. BufferedReader의 readLine()으로 라인 단위 읽기가 가능하다.
// 파일을 라인 단위로 읽기
try (BufferedReader br = new BufferedReader(new FileReader("input.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 라인 단위로 쓰기
try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
bw.write("첫 번째 줄");
bw.newLine(); // 줄바꿈
bw.write("두 번째 줄");
}5.2 InputStreamReader와 OutputStreamWriter
바이트 기반 스트림을 문자 기반 스트림으로 연결하는 다리 역할을 한다. 인코딩을 지정할 수 있다.
// System.in(바이트 스트림)을 문자 스트림으로 변환하여 라인 단위 입력
BufferedReader br = new BufferedReader(
new InputStreamReader(System.in));
String input = br.readLine();
// 인코딩을 지정하여 파일 읽기
InputStreamReader isr = new InputStreamReader(
new FileInputStream("data.txt"), "UTF-8");
// 현재 인코딩 확인
System.out.println(isr.getEncoding()); // UTF-86. 표준 입출력과 File
6.1 표준 입출력
자바 어플리케이션 실행 시 자동으로 생성되는 3가지 스트림이다.
| 스트림 | 타입 | 용도 |
|---|---|---|
System.in | InputStream | 콘솔로부터 데이터 입력 |
System.out | PrintStream | 콘솔로 데이터 출력 |
System.err | PrintStream | 콘솔로 에러 출력 |
실제 내부적으로는 BufferedInputStream과 BufferedOutputStream의 인스턴스를 사용한다.
표준 입출력 대상 변경
| 메서드 | 설명 |
|---|---|
System.setIn(InputStream in) | 입력 대상 변경 |
System.setOut(PrintStream out) | 출력 대상 변경 |
System.setErr(PrintStream err) | 에러 출력 대상 변경 |
// 표준 출력을 파일로 변경
PrintStream ps = new PrintStream(new FileOutputStream("output.log"));
System.setOut(ps);
System.out.println("이 내용은 파일에 출력됩니다");6.2 RandomAccessFile
자바에서 유일하게 하나의 클래스로 읽기/쓰기가 모두 가능한 스트림이다. DataInput과 DataOutput 인터페이스를 구현했기 때문에 기본 자료형 단위로 읽고 쓸 수 있다.
가장 큰 특징은 파일 포인터를 이용하여 파일의 어느 위치에서든 읽기/쓰기가 가능하다는 것이다.
try (RandomAccessFile raf = new RandomAccessFile("data.dat", "rw")) {
// 쓰기
raf.writeInt(100);
raf.writeUTF("Hello");
// 파일 포인터 확인
System.out.println("현재 위치: " + raf.getFilePointer());
// 파일 포인터를 처음으로 이동
raf.seek(0);
// 읽기
int num = raf.readInt();
String str = raf.readUTF();
}| 메서드 | 설명 |
|---|---|
long getFilePointer() | 현재 파일 포인터 위치 반환 |
void seek(long pos) | 파일 포인터를 지정 위치로 이동 |
int skipBytes(int n) | 현재 위치에서 n만큼 건너뛰기 |
6.3 File 클래스
파일과 디렉터리를 다루기 위한 클래스이다. File 인스턴스를 생성해도 실제 파일이 생성되지는 않는다.
// File 인스턴스 생성 (파일이 생성되지는 않음)
File f = new File("C:\\data\\test.txt");
// 새 파일 실제 생성
f.createNewFile();
// 디렉터리 생성
File dir = new File("C:\\data\\newDir");
dir.mkdir(); // 단일 디렉터리 생성
dir.mkdirs(); // 중간 경로까지 모두 생성경로 관련 메서드
| 메서드 | 설명 |
|---|---|
String getName() | 파일 이름 반환 |
String getPath() | 경로 반환 |
String getAbsolutePath() | 절대 경로 반환 |
String getCanonicalPath() | 정규 경로 반환 (기호, 링크 제거) |
String getParent() | 부모 디렉터리 반환 |
File f = new File("./src/../data/test.txt");
System.out.println(f.getPath()); // .\src\..\data\test.txt
System.out.println(f.getAbsolutePath()); // C:\project\.\src\..\data\test.txt
System.out.println(f.getCanonicalPath()); // C:\project\data\test.txt경로 구분자
| 멤버 변수 | Windows | Unix/Linux |
|---|---|---|
File.separator | \ | / |
File.pathSeparator | ; | : |
절대 경로 vs 정규 경로:
- 절대 경로(Absolute Path): 루트부터 시작하는 전체 경로.
.이나..같은 기호가 포함될 수 있어 하나의 파일에 여러 절대 경로가 존재할 수 있다. - 정규 경로(Canonical Path): 기호나 링크를 포함하지 않는 유일한 경로.
File의 유용한 메서드
File f = new File("test.txt");
// 파일 정보
f.exists(); // 존재 여부
f.isFile(); // 파일인지
f.isDirectory(); // 디렉터리인지
f.canRead(); // 읽기 가능 여부
f.canWrite(); // 쓰기 가능 여부
f.length(); // 파일 크기 (byte)
f.lastModified(); // 마지막 수정 시간
// 디렉터리 내 파일 목록
File dir = new File("C:\\data");
String[] fileList = dir.list(); // 파일명 배열
File[] files = dir.listFiles(); // File 객체 배열7. 직렬화 (Serialization)
7.1 직렬화란?
직렬화(Serialization): 객체를 연속적인 데이터(바이트 스트림)로 변환하는 것 역직렬화(Deserialization): 바이트 스트림을 다시 객체로 복원하는 것
직렬화 역직렬화
객체 ──────────▶ 바이트 스트림 ──────────▶ 객체
(인스턴스변수 값) (파일/네트워크 전송) (원래 객체 복원)객체를 저장한다는 것은 모든 인스턴스 변수의 값을 저장하는 것과 같다. 메서드나 클래스 변수는 직렬화 대상이 아니다.
7.2 ObjectInputStream과 ObjectOutputStream
// 직렬화 — 객체를 파일에 저장
try (ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream("user.ser"))) {
oos.writeObject(new UserInfo("홍길동", 25));
}
// 역직렬화 — 파일에서 객체를 읽기
try (ObjectInputStream ois = new ObjectInputStream(
new FileInputStream("user.ser"))) {
UserInfo info = (UserInfo) ois.readObject();
System.out.println(info.getName()); // "홍길동"
}7.3 Serializable 인터페이스
직렬화가 가능한 클래스를 만들려면 java.io.Serializable 인터페이스를 구현해야 한다.
public class UserInfo implements Serializable {
private String name;
private int age;
private transient String password; // 직렬화 제외
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
// getter/setter...
}직렬화 규칙
| 규칙 | 설명 |
|---|---|
| Serializable 구현 | 직렬화하려면 반드시 구현해야 함 |
| 상속 | 조상이 Serializable이면 자손도 직렬화 가능 |
| 조상이 미구현 | 조상의 인스턴스 변수는 직렬화에서 제외 |
| transient | 이 제어자가 붙은 변수는 직렬화 대상에서 제외 |
| static 변수 | 직렬화 대상이 아님 (클래스 변수이므로) |
// 상속과 직렬화
class Parent { // Serializable 미구현
int parentValue = 100;
}
class Child extends Parent implements Serializable {
int childValue = 200;
}
// Child를 직렬화하면 childValue(200)만 저장된다
// parentValue(100)는 직렬화되지 않는다transient를 사용한다. 역직렬화 시 해당 필드는 기본값(null, 0 등)으로 초기화된다.7.4 serialVersionUID
직렬화된 객체를 역직렬화할 때, 클래스의 버전이 일치해야 한다. 버전이 다르면 InvalidClassException이 발생한다.
// 클래스 내에 serialVersionUID를 직접 정의하여 버전 관리
class MyData implements Serializable {
static final long serialVersionUID = 1L;
int value;
String name;
// 필드가 추가되어도 serialVersionUID가 같으면 역직렬화 가능
}| 항목 | 자동 생성 | 수동 정의 |
|---|---|---|
| 생성 방식 | 클래스 멤버 정보 기반 자동 생성 | 개발자가 직접 값 지정 |
| 클래스 변경 시 | 버전이 자동 변경 → 역직렬화 실패 | 버전 유지 가능 |
| 권장 여부 | 비권장 | 권장 |
static 변수 하나만 추가해도 버전이 바뀌어 기존에 직렬화된 데이터를 읽을 수 없게 된다. transient, static 변수 추가처럼 직렬화에 영향을 주지 않는 변경의 경우에도 버전이 달라지는 문제가 있다.8. 요약
스트림 체계 한눈에 보기
스트림
┌─────────────┴─────────────┐
바이트 기반 문자 기반
┌─────┴─────┐ ┌─────┴─────┐
InputStream OutputStream Reader Writer
│ │ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
File ByteArr File ByteArr File CharArr File CharArr
Piped Piped Piped Piped
String String
보조 스트림 (기반 스트림을 감싸서 기능 추가)
┌─────────────────────────────────────────┐
│ Buffered — 버퍼를 이용한 성능 향상 │
│ Data — 기본 자료형 단위 읽기/쓰기 │
│ Object — 객체 직렬화/역직렬화 │
│ Print — 다양한 출력 형식 │
│ InputStreamReader — 바이트→문자 변환 │
└─────────────────────────────────────────┘핵심 개념 정리
| 개념 | 설명 |
|---|---|
| 스트림 | 데이터를 운반하는 단방향 연결 통로 |
| 바이트 스트림 | 1 byte 단위. InputStream / OutputStream |
| 문자 스트림 | 2 byte(char) 단위. Reader / Writer |
| 보조 스트림 | 기반 스트림을 감싸서 기능 향상 (Buffered, Data 등) |
| 표준 입출력 | System.in / System.out / System.err |
| RandomAccessFile | 파일 포인터로 임의 위치 읽기/쓰기 |
| File | 파일/디렉터리 정보 조회 및 관리 |
| 직렬화 | 객체 → 바이트 스트림. Serializable 구현 필요 |
| transient | 직렬화에서 제외할 필드에 사용 |
| serialVersionUID | 직렬화 클래스의 버전 관리. 직접 정의 권장 |
스트림 선택 가이드
바이너리 데이터 (이미지, 동영상 등)
└→ FileInputStream / FileOutputStream + Buffered
텍스트 데이터 (txt, csv, log 등)
└→ FileReader / FileWriter + BufferedReader / BufferedWriter
콘솔 입력
└→ BufferedReader + InputStreamReader(System.in)
└→ 또는 Scanner (JDK 1.5+)
객체 저장/전송
└→ ObjectOutputStream / ObjectInputStream
기본 자료형 저장
└→ DataOutputStream / DataInputStream
파일 임의 접근
└→ RandomAccessFile