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 - -
권장 시나리오 레거시, 간단한 작업 고성능, 논블로킹 일반적인 파일 작업

카테고리:

업데이트: