본문으로 건너뛰기

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 / OutputStream1 byte이미지, 동영상, 모든 파일
문자 기반Reader / Writer2 byte (char)텍스트 전용

2.2 방향에 따른 분류

방향바이트 기반문자 기반
입력InputStreamReader
출력OutputStreamWriter

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.iojava.nio
방식스트림 기반 (단방향)채널/버퍼 기반 (양방향)
블로킹블로킹 I/O논블로킹 가능
데이터 단위바이트/문자 단위버퍼(Buffer) 단위
사용 편의성간단하고 직관적코드가 복잡하나 성능 우수
권장 상황간단한 파일 I/O고성능 서버, 대용량 파일
주요 클래스FileInputStreamFileChannel, ByteBuffer