Java I/O 완전 정리
Java의 I/O 시스템은 데이터를 읽고 쓰는 모든 작업의 근간입니다. 고전적인 java.io부터 고성능 NIO, 편리한 NIO.2까지 전체 체계를 상세히 정리합니다.
1. I/O 기본 개념
I/O(Input/Output)는 프로그램이 외부 데이터 소스(파일, 네트워크, 키보드 등)와 데이터를 주고받는 행위입니다.
스트림(Stream)이란?
스트림은 데이터가 흐르는 단방향 통로입니다. 물이 파이프를 통해 흐르듯, 데이터는 스트림을 통해 순차적으로 이동합니다.
[데이터 소스] ──스트림──▶ [프로그램] ← 입력 스트림 (InputStream)
[프로그램] ──스트림──▶ [데이터 목적지] ← 출력 스트림 (OutputStream)
핵심 특성
- 단방향: 입력과 출력은 별개의 스트림
- 순차적: 데이터를 순서대로 읽고 씀 (Random Access는 별도 처리)
- 블로킹(Blocking): 데이터가 준비될 때까지 스레드가 대기 (기본 java.io)
2. 바이트 스트림 vs 문자 스트림
Java I/O는 처리 단위에 따라 두 계열로 나뉩니다.
┌─────────────────────────────────────────────────────────┐
│ Java I/O 계층 │
├───────────────────────┬─────────────────────────────────┤
│ 바이트 스트림 │ 문자 스트림 │
│ (Byte Stream) │ (Character Stream) │
├───────────────────────┼─────────────────────────────────┤
│ InputStream │ Reader │
│ OutputStream │ Writer │
│ │ │
│ 처리 단위: 1 byte │ 처리 단위: 2 bytes (char, UTF-16)│
│ 모든 데이터 처리 가능 │ 텍스트 데이터 처리 특화 │
│ 이미지, 영상, 음성 등 │ 문자 인코딩 자동 처리 │
└───────────────────────┴─────────────────────────────────┘
2.1 바이트 스트림 계층 구조
InputStream (추상)
├── FileInputStream // 파일에서 바이트 읽기
├── ByteArrayInputStream // 바이트 배열에서 읽기
├── PipedInputStream // 파이프 통신
└── FilterInputStream (추상) // 보조 스트림의 부모
├── BufferedInputStream // 버퍼링
├── DataInputStream // 기본형 타입 읽기
├── PushbackInputStream // 읽은 데이터 되돌리기
└── CheckedInputStream // 체크섬 계산
OutputStream (추상)
├── FileOutputStream
├── ByteArrayOutputStream
├── PipedOutputStream
└── FilterOutputStream (추상)
├── BufferedOutputStream
├── DataOutputStream
├── PrintStream // System.out 이 바로 이것
└── CheckedOutputStream
2.2 문자 스트림 계층 구조
Reader (추상)
├── InputStreamReader // 바이트→문자 변환 (Bridge)
│ └── FileReader // 파일에서 문자 읽기
├── BufferedReader // 버퍼링 + readLine()
├── CharArrayReader
├── StringReader
└── PipedReader
Writer (추상)
├── OutputStreamWriter // 문자→바이트 변환 (Bridge)
│ └── FileWriter
├── BufferedWriter // 버퍼링 + newLine()
├── CharArrayWriter
├── StringWriter
├── PrintWriter // printf, println 지원
└── PipedWriter
2.3 기본 사용 예제
// 바이트 스트림 - 파일 읽기
try (FileInputStream fis = new FileInputStream("data.bin")) {
int b;
while ((b = fis.read()) != -1) { // read()는 0~255 또는 -1(EOF)
System.out.print(b + " ");
}
}
// 문자 스트림 - 파일 읽기
try (FileReader fr = new FileReader("text.txt", StandardCharsets.UTF_8)) {
int ch;
while ((ch = fr.read()) != -1) { // read()는 문자(char) 또는 -1(EOF)
System.out.print((char) ch);
}
}
3. 보조 스트림 — 데코레이터 패턴
보조 스트림(Filter Stream)은 기본 스트림을 감싸서 추가 기능을 제공합니다. 이는 데코레이터 패턴(Decorator Pattern)의 전형적인 예입니다.
┌──────────────────────────────────────────────────────────────┐
│ 데코레이터 패턴 구조 │
│ │
│ FileInputStream │
│ │ │
│ ▼ │
│ BufferedInputStream ← 버퍼링 추가 │
│ │ │
│ ▼ │
│ DataInputStream ← 기본형 타입 읽기 추가 │
│ │ │
│ ▼ │
│ [읽기 작업] ← 최종적으로 모든 기능 사용 가능 │
└──────────────────────────────────────────────────────────────┘
3.1 BufferedInputStream / BufferedReader
버퍼를 사용해 I/O 횟수를 줄여 성능을 크게 향상시킵니다.
// 기본 버퍼 크기: 8192 bytes
try (BufferedReader br = new BufferedReader(
new FileReader("large_file.txt", StandardCharsets.UTF_8))) {
String line;
while ((line = br.readLine()) != null) { // 줄 단위 읽기
System.out.println(line);
}
}
// 커스텀 버퍼 크기 (16KB)
try (BufferedInputStream bis = new BufferedInputStream(
new FileInputStream("data.bin"), 16384)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
// buffer 처리
}
}
3.2 DataInputStream / DataOutputStream
Java 기본형 데이터를 바이너리로 읽고 씁니다.
// 쓰기
try (DataOutputStream dos = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
dos.writeInt(42);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("Hello, Java!"); // 변형 UTF-8 형식
}
// 읽기 (쓴 순서와 동일하게)
try (DataInputStream dis = new DataInputStream(
new BufferedInputStream(new FileInputStream("data.bin")))) {
int i = dis.readInt();
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.printf("int=%d, double=%.2f, boolean=%b, string=%s%n", i, d, b, s);
}
3.3 PrintWriter / PrintStream
형식화된 출력을 지원합니다.
try (PrintWriter pw = new PrintWriter(
new BufferedWriter(new FileWriter("output.txt")), true)) {
pw.println("첫 번째 줄");
pw.printf("이름: %s, 나이: %d%n", "김자바", 30);
pw.format("금액: %,.0f원%n", 1000000.0);
}
4. 파일 I/O
4.1 FileInputStream / FileOutputStream
// 파일 복사 (바이트 스트림)
public static void copyFile(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
}
4.2 FileReader / FileWriter
// 텍스트 파일 복사 (문자 스트림)
public static void copyTextFile(String src, String dest) throws IOException {
try (BufferedReader reader = new BufferedReader(
new FileReader(src, StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(
new FileWriter(dest, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
}
}
4.3 RandomAccessFile
파일의 임의 위치에 읽고 쓸 수 있습니다.
try (RandomAccessFile raf = new RandomAccessFile("data.bin", "rw")) {
// 10번째 바이트부터 읽기
raf.seek(10);
int value = raf.readInt();
// 파일 끝에 추가
raf.seek(raf.length());
raf.writeUTF("추가 데이터");
System.out.println("파일 포인터 위치: " + raf.getFilePointer());
}
5. NIO (java.nio) — Non-blocking I/O
Java 1.4에서 도입된 NIO는 기존 I/O의 성능 한계를 극복합니다.
┌─────────────────────────────────────────────────────────┐
│ NIO 핵심 구성 요소 │
│ │
│ ┌──────────┐ 읽기/쓰기 ┌──────────┐ │
│ │ Buffer │◀────────────▶│ Channel │ │
│ └──────────┘ └──────────┘ │
│ │ │ │
│ 데이터를 담는 컨테이너 실제 I/O를 수행하는 통로 │
│ │ │
│ ┌───────┴──────┐ │
│ │ Selector │ │
│ └──────────────┘ │
│ 다수의 채널을 하나의 스레드로 관리 │
└─────────────────────────────────────────────────────────┘
5.1 Buffer
Buffer는 데이터를 임시 저장하는 컨테이너입니다. 핵심 속성은 capacity, position, limit, mark입니다.
Buffer 상태 변화:
초기 (capacity=10):
position limit/capacity
↓ ↓
[ _ | _ | _ | _ | _ | _ | _ | _ | _ | _ ]
데이터 3개 쓰기 후:
position limit/capacity
↓ ↓
[ A | B | C | _ | _ | _ | _ | _ | _ | _ ]
flip() 호출 후 (읽기 모드로 전환):
position limit capacity
↓ ↓ ↓
[ A | B | C | _ | _ | _ | _ | _ | _ | _ ]
clear() 호출 후 (다시 쓰기 모드):
position limit/capacity
↓ ↓
[ A | B | C | _ | _ | _ | _ | _ | _ | _ ] (내용은 남아 있음)
// ByteBuffer 기본 사용
ByteBuffer buffer = ByteBuffer.allocate(10); // 힙 버퍼
// ByteBuffer buffer = ByteBuffer.allocateDirect(10); // 다이렉트 버퍼 (OS 메모리)
// 데이터 쓰기
buffer.put((byte) 'H');
buffer.put((byte) 'i');
buffer.putInt(42);
System.out.println("쓰기 후 position: " + buffer.position()); // 6
// 읽기 모드로 전환
buffer.flip();
System.out.println("flip 후 position: " + buffer.position()); // 0
System.out.println("flip 후 limit: " + buffer.limit()); // 6
byte b1 = buffer.get(); // 'H'
byte b2 = buffer.get(); // 'i'
int num = buffer.getInt(); // 42
// 버퍼 재사용
buffer.clear(); // position=0, limit=capacity (내용은 유지)
buffer.rewind(); // position=0, limit 유지 (읽기 반복)
buffer.compact(); // 읽지 않은 데이터를 앞으로 이동
5.2 Channel
Channel은 양방향 데이터 통로로, 반드시 Buffer와 함께 사용합니다.
// FileChannel로 파일 읽기
try (FileChannel fc = FileChannel.open(
Paths.get("data.txt"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (fc.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
}
// FileChannel로 파일 복사 (transferTo 사용 - OS 레벨 최적화)
try (FileChannel src = FileChannel.open(Paths.get("source.txt"), StandardOpenOption.READ);
FileChannel dst = FileChannel.open(Paths.get("dest.txt"),
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
src.transferTo(0, src.size(), dst); // Zero-Copy 전송
}
5.3 Selector — 논블로킹 멀티플렉싱
하나의 스레드로 여러 채널을 감시합니다.
// Selector 기본 구조
Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false); // 논블로킹 모드
// 채널을 셀렉터에 등록 (ACCEPT 이벤트 감시)
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int readyChannels = selector.select(); // 이벤트 발생까지 블로킹
if (readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
// 클라이언트 연결 수락
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 데이터 읽기
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
int bytesRead = client.read(buffer);
if (bytesRead == -1) {
client.close();
} else {
buffer.flip();
client.write(buffer); // 에코 서버
}
}
}
}
6. NIO.2 (java.nio.file) — 현대적 파일 API
Java 7에서 도입된 NIO.2는 파일 시스템 작업을 훨씬 편리하게 만들었습니다.
6.1 Path
// Path 생성
Path p1 = Path.of("/home/user/documents/file.txt"); // Java 11+
Path p2 = Paths.get("/home/user", "documents", "file.txt"); // Java 7+
Path p3 = Path.of("relative/path/to/file.txt");
// Path 조작
System.out.println(p1.getFileName()); // file.txt
System.out.println(p1.getParent()); // /home/user/documents
System.out.println(p1.getRoot()); // /
System.out.println(p1.getNameCount()); // 4
System.out.println(p1.getName(0)); // home
System.out.println(p1.subpath(1, 3)); // user/documents
// 경로 결합
Path base = Path.of("/home/user");
Path resolved = base.resolve("documents/file.txt"); // /home/user/documents/file.txt
Path relativized = base.relativize(resolved); // documents/file.txt
// 정규화
Path p = Path.of("/home/user/../user/./documents");
System.out.println(p.normalize()); // /home/user/documents
System.out.println(p.toAbsolutePath()); // 절대 경로로 변환
6.2 Files 유틸리티 클래스
Path path = Path.of("example.txt");
// === 파일 존재 여부 및 속성 ===
Files.exists(path)
Files.notExists(path)
Files.isReadable(path)
Files.isWritable(path)
Files.isDirectory(path)
Files.isRegularFile(path)
Files.size(path)
Files.getLastModifiedTime(path)
// === 파일 읽기 ===
// 전체 내용을 String으로 (Java 11+)
String content = Files.readString(path, StandardCharsets.UTF_8);
// 전체 내용을 바이트 배열로
byte[] bytes = Files.readAllBytes(path);
// 모든 줄을 List로
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// 스트림으로 줄 단위 읽기 (대용량 파일)
try (Stream<String> stream = Files.lines(path, StandardCharsets.UTF_8)) {
stream.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
}
// === 파일 쓰기 ===
// String 쓰기 (Java 11+)
Files.writeString(path, "내용", StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// 바이트 배열 쓰기
Files.write(path, bytes, StandardOpenOption.CREATE);
// 여러 줄 쓰기
List<String> lines2 = List.of("첫째 줄", "둘째 줄", "셋째 줄");
Files.write(path, lines2, StandardCharsets.UTF_8);
// BufferedWriter 얻기
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("내용");
}
6.3 파일 복사 / 이동 / 삭제
Path src = Path.of("source.txt");
Path dst = Path.of("destination.txt");
Path dir = Path.of("backup");
// 복사
Files.copy(src, dst); // 이미 존재하면 예외
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); // 덮어쓰기
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES); // 메타데이터도 복사
// 이동 (rename 포함)
Files.move(src, dst);
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE); // 원자적 이동
// 삭제
Files.delete(path); // 없으면 NoSuchFileException
Files.deleteIfExists(path); // 없어도 예외 없음
// 디렉토리 생성
Files.createDirectory(dir); // 부모 없으면 예외
Files.createDirectories(dir); // 부모도 함께 생성
6.4 디렉토리 탐색
Path dir = Path.of("/home/user/documents");
// 단순 목록 (1레벨)
try (Stream<Path> entries = Files.list(dir)) {
entries.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// 재귀 탐색 (깊이 제한)
try (Stream<Path> walk = Files.walk(dir, 3)) { // 최대 3레벨
walk.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
// 패턴 매칭 탐색
try (Stream<Path> found = Files.find(dir, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile() && path.toString().endsWith(".log"))) {
found.forEach(System.out::println);
}
// FileVisitor - 정교한 제어
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println("파일: " + file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
System.out.println("디렉토리 진입: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("접근 실패: " + file);
return FileVisitResult.CONTINUE;
}
});
6.5 WatchService — 파일 시스템 감시
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Path.of("./watched");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
System.out.println("디렉토리 감시 시작...");
while (true) {
WatchKey key = watcher.take(); // 이벤트 대기 (블로킹)
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
System.out.printf("[%s] %s%n", kind.name(), filename);
}
if (!key.reset()) break; // 더 이상 감시 불가
}
7. 직렬화 (Serialization)
직렬화는 객체의 상태를 바이트 스트림으로 변환하는 과정입니다. 역직렬화는 그 반대입니다.
직렬화 과정:
┌─────────────────┐ ┌─────────────────┐
│ Java Object │──직렬화(Serialize)──▶│ byte stream │
│ (메모리 상주) │ │ (파일/네트워크) │
└─────────────────┘ └─────────────────┘
▲ │
│ 역직렬화(Deserialize) │
└──────────────────────────────────────┘
7.1 Serializable 인터페이스
import java.io.Serializable;
public class Person implements Serializable {
// serialVersionUID: 직렬화 버전 식별자
// 명시하지 않으면 JVM이 자동 생성하나, 클래스 변경 시 달라질 수 있음
private static final long serialVersionUID = 1L;
private String name;
private int age;
private transient String password; // 직렬화 제외
private static String company; // static은 직렬화 안 됨
public Person(String name, int age, String password) {
this.name = name;
this.age = age;
this.password = password;
}
@Override
public String toString() {
return String.format("Person{name='%s', age=%d, password='%s'}",
name, age, password);
}
}
7.2 직렬화 / 역직렬화 수행
// 직렬화
Person person = new Person("김자바", 30, "secret123");
try (ObjectOutputStream oos = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("person.ser")))) {
oos.writeObject(person);
System.out.println("직렬화 완료");
}
// 역직렬화
try (ObjectInputStream ois = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("person.ser")))) {
Person restored = (Person) ois.readObject();
System.out.println(restored);
// Person{name='김자바', age=30, password='null'}
// transient 필드는 null(기본값)로 복원됨
}
7.3 커스텀 직렬화
public class SecurePerson implements Serializable {
private static final long serialVersionUID = 2L;
private String name;
private transient String encryptedPassword;
// 직렬화 시 자동 호출
private void writeObject(ObjectOutputStream oos) throws IOException {
oos.defaultWriteObject(); // 기본 직렬화
// 암호화 후 저장
oos.writeObject(encrypt(encryptedPassword));
}
// 역직렬화 시 자동 호출
private void readObject(ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject(); // 기본 역직렬화
// 복호화
encryptedPassword = decrypt((String) ois.readObject());
}
private String encrypt(String data) { return "ENC:" + data; }
private String decrypt(String data) { return data.replace("ENC:", ""); }
}
7.4 serialVersionUID와 버전 관리
// 버전 1 (저장)
public class Config implements Serializable {
private static final long serialVersionUID = 1L;
private String host;
private int port;
}
// 버전 2 (필드 추가 - 같은 serialVersionUID 유지 시 하위 호환)
public class Config implements Serializable {
private static final long serialVersionUID = 1L; // 동일하게 유지
private String host;
private int port;
private String protocol = "HTTP"; // 새 필드 (역직렬화 시 기본값으로 초기화)
}
// 주의: serialVersionUID를 변경하면 이전 파일 역직렬화 시 InvalidClassException 발생
8. try-with-resources와 AutoCloseable
Java 7에서 도입된 try-with-resources는 자원을 자동으로 닫아줍니다.
// 전통적 방식 (자원 누수 위험)
FileReader fr = null;
try {
fr = new FileReader("file.txt");
// ... 작업
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fr != null) {
try { fr.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
// try-with-resources (Java 7+)
try (FileReader fr = new FileReader("file.txt");
BufferedReader br = new BufferedReader(fr)) {
// ... 작업 (예외 발생 여부와 무관하게 자동으로 close() 호출)
}
// 여러 자원: 선언 역순으로 close() 호출됨
// 위 예시에서: br.close() → fr.close()
8.1 AutoCloseable 구현
public class DatabaseConnection implements AutoCloseable {
private final String url;
private boolean connected;
public DatabaseConnection(String url) {
this.url = url;
this.connected = true;
System.out.println("연결: " + url);
}
public void query(String sql) {
if (!connected) throw new IllegalStateException("연결이 닫혔습니다");
System.out.println("쿼리 실행: " + sql);
}
@Override
public void close() {
if (connected) {
connected = false;
System.out.println("연결 종료: " + url);
}
}
}
// 사용
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/db")) {
conn.query("SELECT * FROM users");
} // 자동으로 close() 호출
8.2 Suppressed Exception
// try 블록과 close() 모두 예외 발생 시
try (AutoCloseable resource = new AutoCloseable() {
@Override
public void close() throws Exception {
throw new Exception("close 예외");
}
}) {
throw new RuntimeException("본문 예외");
} catch (Exception e) {
System.out.println("주 예외: " + e.getMessage()); // "본문 예외"
// close()의 예외는 suppressed로 첨부됨
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("억제된 예외: " + suppressed.getMessage()); // "close 예외"
}
}
9. 메모리 맵 파일 (MappedByteBuffer)
파일을 메모리에 매핑하여 OS의 가상 메모리를 직접 사용합니다. 대용량 파일 처리에 매우 효율적입니다.
메모리 맵 파일 구조:
┌─────────────────────────────────────────────────────┐
│ JVM Process │
│ ┌─────────────────────────────────────────────┐ │
│ │ 가상 메모리 주소 공간 │ │
│ │ ┌──────────────────┐ │ │
│ │ │ MappedByteBuffer│──▶ 직접 파일 접근 │ │
│ │ └──────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ OS Page Cache │ │
│ │ ┌────────────────────────────────────┐ │ │
│ │ │ 파일 데이터 │ │ │
│ │ └────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
→ 파일 복사 없이 OS 페이지 캐시를 직접 접근 (Zero-Copy)
// 대용량 파일 읽기
try (FileChannel fc = FileChannel.open(Path.of("large_file.bin"), StandardOpenOption.READ)) {
long fileSize = fc.size();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
// 파일 전체가 메모리에 매핑됨 (실제 로드는 필요 시 on-demand)
while (mbb.hasRemaining()) {
byte b = mbb.get(); // OS 페이지 캐시에서 직접 읽기
// 처리...
}
}
// 읽기/쓰기 매핑
try (FileChannel fc = FileChannel.open(Path.of("data.bin"),
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
mbb.putInt(0, 42); // 오프셋 0에 int 쓰기
mbb.putLong(4, 100L); // 오프셋 4에 long 쓰기
mbb.force(); // 변경사항을 파일에 강제 동기화
}
// 대용량 파일 처리 (1GB 이상): 청크 단위로 매핑
try (FileChannel fc = FileChannel.open(Path.of("huge_file.bin"), StandardOpenOption.READ)) {
long fileSize = fc.size();
long chunkSize = 256 * 1024 * 1024L; // 256MB 청크
long position = 0;
while (position < fileSize) {
long size = Math.min(chunkSize, fileSize - position);
MappedByteBuffer chunk = fc.map(FileChannel.MapMode.READ_ONLY, position, size);
// 청크 처리...
position += size;
}
}
10. 성능 비교: IO vs NIO vs NIO.2
파일 복사 성능 비교 (1GB 파일, 참고값):
방법 | 속도 | 메모리 사용 | 코드 복잡도
──────────────────────────────────────────────────────────────
FileInputStream (버퍼 없음) | 느림 | 낮음 | 낮음
FileInputStream + 버퍼 | 보통 | 낮음 | 보통
FileChannel.transferTo | 빠름 | 낮음 | 보통
MappedByteBuffer | 매우빠름| 높음 | 높음
Files.copy (NIO.2) | 빠름 | 낮음 | 매우낮음 ← 권장
10.1 상황별 권장 API
// 1. 소용량 텍스트 파일 읽기/쓰기 → Files 유틸리티
String content = Files.readString(Path.of("small.txt"));
Files.writeString(Path.of("output.txt"), content);
// 2. 대용량 텍스트 파일 → BufferedReader + Files.lines
try (Stream<String> lines = Files.lines(Path.of("large.txt"))) {
lines.parallel() // 병렬 처리도 가능
.map(String::toUpperCase)
.forEach(System.out::println);
}
// 3. 이진 파일 복사 → Files.copy
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
// 4. 고성능 파일 복사 → transferTo (Zero-Copy)
try (FileChannel in = FileChannel.open(src, StandardOpenOption.READ);
FileChannel out = FileChannel.open(dst,
StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
in.transferTo(0, in.size(), out);
}
// 5. 임의 위치 읽기/쓰기 → FileChannel + seek
try (FileChannel fc = FileChannel.open(path,
StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ByteBuffer buf = ByteBuffer.allocate(8);
fc.read(buf, 100); // 오프셋 100에서 읽기
buf.flip();
fc.write(buf, 200); // 오프셋 200에 쓰기
}
// 6. 초대용량 파일 처리 → MappedByteBuffer
// (파일이 OS 페이지 캐시에 수용 가능한 크기일 때)
11. 전체 구조 요약
┌─────────────────────────────────────────────────────────────────────┐
│ Java I/O 전체 맵 │
│ │
│ java.io java.nio java.nio.file │
│ ────────────────────────────────────────────────────────────── │
│ 바이트 스트림 Buffer Path │
│ InputStream ByteBuffer Files │
│ OutputStream CharBuffer FileSystem │
│ IntBuffer, ... WatchService │
│ 문자 스트림 │
│ Reader Channel FileSystems │
│ Writer FileChannel │
│ SocketChannel StandardCopyOption │
│ 보조 스트림 ServerSocketChannel StandardOpenOption │
│ Buffered* DatagramChannel │
│ Data* │
│ Print* Selector BasicFileAttributes │
│ SelectionKey FileVisitor │
│ 직렬화 │
│ ObjectInput/OutputStream MappedByteBuffer SimpleFileVisitor │
│ Serializable │
│ Externalizable Charset FileVisitResult │
│ CharsetEncoder │
│ RandomAccessFile CharsetDecoder │
└─────────────────────────────────────────────────────────────────────┘
권장 사용 가이드:
텍스트 읽기/쓰기 → Files.readString / writeString / lines
파일 복사/이동 → Files.copy / move
고성능 I/O → FileChannel + ByteBuffer
논블로킹 서버 → Selector + SocketChannel
대용량 파일 처리 → MappedByteBuffer 또는 Files.lines (Stream)
디렉토리 탐색 → Files.walk / walkFileTree
파일 변경 감시 → WatchService
핵심 정리
| 항목 | java.io | java.nio | java.nio.file |
|---|---|---|---|
| 도입 | Java 1.0 | Java 1.4 | Java 7 |
| 처리 방식 | 스트림 | 버퍼/채널 | Path 기반 |
| 블로킹 | 항상 블로킹 | 논블로킹 가능 | 동기 (내부적으로 채널) |
| 성능 | 보통 | 높음 | 높음 (편의성 우선) |
| 사용 편의성 | 보통 | 복잡 | 매우 높음 |
| 주요 클래스 | InputStream/Reader | ByteBuffer/FileChannel | Files/Path |
| 직렬화 | ObjectOutputStream | - | - |
| 권장 시나리오 | 레거시, 간단한 작업 | 고성능, 논블로킹 | 일반적인 파일 작업 |