3주차 Unit 7.2 — IO vs NIO (역사적 진화)

Psj·2026년 5월 19일

F-lab

목록 보기
102/237

Unit 7.2 — IO vs NIO (역사적 진화)

F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • IO (Java 1.0) 의 핵심 구조와 한계는?
  • NIO (Java 1.4) 가 등장한 배경과 해결한 문제는?
  • NIO.2 (Java 7) 의 추가 기능은?
  • 3 시대 (IO, NIO, NIO.2) 의 4가지 핵심 차이는?
  • File 클래스 vs Files 유틸리티 의 결정적 차이는?
  • 단방향 vs 양방향 I/O 의 의미와 효과는?
  • Blocking vs Non-blocking 의 개요는? (정밀은 Unit 7.4)
  • Decorator 패턴 이 IO 에서 어떻게 활용되는가?
  • 실무에서 IO/NIO/NIO.2 선택 가이드 는?

🎯 핵심 한 문장

자바 I/O 는 1.0 의 java.io (스트림), 1.4 의 java.nio (채널+버퍼), 7 의 java.nio.file (NIO.2) 로 3 단계 진화했다.
IO 는 스트림 기반, 1바이트씩, 단방향, Blocking 만 으로 25년 전 디자인의 한계가 있었고,
NIO 는 채널 + 버퍼, 블록 단위, 양방향, Non-blocking 가능 으로 대규모 서버에 대응,
NIO.2 는 Path, Files (static), Stream 통합, 비동기 I/O 로 현대적 파일 API 완성.
셋 다 살아있고, 상황에 따라 선택 — 단순한 파일은 NIO.2, 대규모 네트워크는 NIO, 레거시 호환은 IO.

비유 — 우편 시스템의 진화

IO (Java 1.0, 1996) — 옛 우편국
  - 편지 한 장씩 처리 (1바이트)
  - 보내는 것과 받는 것 따로 (단방향)
  - 줄 서서 대기 (Blocking)
  - 누군가 줄에 있으면 모두 대기

NIO (Java 1.4, 2002) — 현대 우체국
  - 묶음 단위 처리 (Buffer = 블록)
  - 한 창구에서 양방향 (Channel)
  - 줄 안 서도 됨 (Non-blocking)
  - 한 직원이 여러 손님 (Selector)

NIO.2 (Java 7, 2011) — 디지털 우체국
  - 파일 시스템 추상화 (Path)
  - 한 줄로 모든 작업 (Files static)
  - Stream 통합 (Files.lines)
  - 비동기 처리 (AsynchronousChannel)

→ 3 시대의 진화 = 한계 → 해결.


🧭 9개 섹션 로드맵

1. IO 의 등장과 구조 (Java 1.0)
2. IO 의 5가지 한계
3. NIO 의 등장과 핵심 개념 (Java 1.4)
4. NIO.2 의 등장과 추가 기능 (Java 7)
5. 3 시대 종합 비교
6. File 클래스 vs Files 유틸리티
7. 단방향 vs 양방향, Blocking vs Non-blocking
8. 실무 활용 가이드
9. 면접 + 자기 점검

1️⃣ IO 의 등장과 구조 (Java 1.0)

1.1 IO 의 등장

java.io (Java 1.0, 1996):

  자바 최초의 I/O API.
  당시 표준 디자인:
  - C 의 stdio 영향
  - 스트림 기반 사고
  - 단방향, 1바이트씩
  - 모든 I/O 가 Blocking

1.2 IO 의 핵심 구조

java.io 의 클래스 계층:

바이트 스트림:
  InputStream         (추상)
    ├── FileInputStream
    ├── ByteArrayInputStream
    ├── PipedInputStream
    ├── FilterInputStream
    │   ├── BufferedInputStream
    │   ├── DataInputStream
    │   └── PushbackInputStream
    └── ObjectInputStream
  
  OutputStream        (추상)
    ├── FileOutputStream
    ├── ByteArrayOutputStream
    ├── PipedOutputStream
    ├── FilterOutputStream
    │   ├── BufferedOutputStream
    │   ├── DataOutputStream
    │   └── PrintStream
    └── ObjectOutputStream

문자 스트림:
  Reader              (추상)
    ├── FileReader
    ├── StringReader
    ├── CharArrayReader
    ├── InputStreamReader
    └── BufferedReader
  
  Writer              (추상)
    ├── FileWriter
    ├── StringWriter
    ├── CharArrayWriter
    ├── OutputStreamWriter
    ├── BufferedWriter
    └── PrintWriter

1.3 바이트 vs 문자 스트림

바이트 스트림 (InputStream, OutputStream):
  - 1바이트 단위
  - 모든 데이터 (이미지, 음악, 텍스트)
  - 인코딩 처리 X

문자 스트림 (Reader, Writer):
  - 1문자 단위 (Java 의 char, 2바이트)
  - 텍스트 전용
  - 인코딩 처리 자동

변환:
  InputStreamReader: InputStream → Reader
  OutputStreamWriter: OutputStream → Writer
  
  예:
  Reader r = new InputStreamReader(System.in, "UTF-8");

1.4 Decorator 패턴

// java.io 의 핵심 디자인 패턴
// "기본 스트림 + 보조 스트림" 조합

// 기본: FileInputStream (파일에서 1바이트씩)
FileInputStream fis = new FileInputStream("file.txt");

// 보조 1: 버퍼 추가
BufferedInputStream bis = new BufferedInputStream(fis);

// 보조 2: 데이터 타입 처리
DataInputStream dis = new DataInputStream(bis);

// 또는 한 줄로 (중첩)
DataInputStream dis = new DataInputStream(
    new BufferedInputStream(
        new FileInputStream("file.txt")));

// 읽기
int value = dis.readInt();

특징:

  • 기본 스트림에 보조 스트림 입혀서 기능 추가
  • 유연하지만 중첩이 깊음

1.5 IO 의 기본 사용

// 파일 읽기
try (FileInputStream fis = new FileInputStream("input.txt")) {
    int b;
    while ((b = fis.read()) != -1) {
        System.out.print((char) b);
    }
}

// 파일 쓰기
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    fos.write("Hello".getBytes());
}

// 텍스트 읽기 (한 줄씩)
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("Hello\n");
    bw.write("World\n");
}

1.6 IO 의 직렬화 지원

// 객체 직렬화 (Java 1.1+)
class Shipment implements Serializable {
    private Long id;
    private String blNo;
    // ...
}

// 저장
try (ObjectOutputStream oos = new ObjectOutputStream(
        new FileOutputStream("shipment.dat"))) {
    oos.writeObject(shipment);
}

// 복원
try (ObjectInputStream ois = new ObjectInputStream(
        new FileInputStream("shipment.dat"))) {
    Shipment loaded = (Shipment) ois.readObject();
}

→ Unit 9.4 에서 정밀.

1.7 IO 의 단점 미리보기

IO 의 한계 (다음 섹션에서 정밀):

1. 1바이트씩 → 느림
2. 단방향 → InputStream/OutputStream 따로
3. Blocking only → 동시성 ↓
4. 중첩 패턴 → 가독성 ↓
5. File 의 한계 → Unit 7.5 에서 정밀

1.8 ILIC 의 IO 활용

// ILIC 의 직렬화
public class ShipmentSerializer {
    
    public void serialize(Shipment shipment, Path dest) throws IOException {
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new BufferedOutputStream(
                    new FileOutputStream(dest.toFile())))) {
            oos.writeObject(shipment);
        }
    }
    
    public Shipment deserialize(Path src) throws IOException, ClassNotFoundException {
        try (ObjectInputStream ois = new ObjectInputStream(
                new BufferedInputStream(
                    new FileInputStream(src.toFile())))) {
            return (Shipment) ois.readObject();
        }
    }
}

// Decorator 패턴 — 중첩
// 1. FileInputStream — 기본 파일
// 2. BufferedInputStream — 버퍼링
// 3. ObjectInputStream — 객체 복원

1.9 자기 점검 답변

IO (java.io) 의 핵심 구조는?

:
1. 스트림 기반:

  • 1바이트 (또는 1문자) 단위
  • InputStream/OutputStream (바이트)
  • Reader/Writer (문자)
  1. 단방향:

    • Input 과 Output 따로
    • 양방향이 필요하면 두 스트림
  2. Blocking only:

    • 데이터 올 때까지 대기
    • 다른 일 못 함
  3. Decorator 패턴:

    • 기본 스트림 + 보조 스트림
    • 중첩으로 기능 추가
  4. 두 가지 스트림:

    • 바이트 (모든 데이터)
    • 문자 (텍스트, 인코딩)

2️⃣ IO 의 5가지 한계

2.1 한계 1 — 1바이트씩 처리의 느림

// IO 의 기본 read() — 1바이트
FileInputStream fis = new FileInputStream("large.dat");
int b;
while ((b = fis.read()) != -1) {
    // 1바이트 처리
}

// 1MB 파일 = 1,048,576번 호출
// 각 호출이 OS 의 system call
// → 매우 느림
// 해결: BufferedInputStream
BufferedInputStream bis = new BufferedInputStream(fis);
int b;
while ((b = bis.read()) != -1) {
    // 여전히 1바이트씩 호출하지만
    // 내부적으로 8KB 단위로 OS 호출
    // → 1MB / 8KB = 128번 호출
}

// 또는 byte[] 활용
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
    // 한 번에 8192바이트
}

→ 보조 스트림 (BufferedInputStream) 으로 일부 해결.

2.2 한계 2 — 단방향성

// 같은 파일을 읽고 쓰기 위해 두 스트림 필요
FileInputStream fis = new FileInputStream("file.txt");
FileOutputStream fos = new FileOutputStream("file.txt");

// 또는 동시 가능한 RandomAccessFile (드물게 사용)
RandomAccessFile raf = new RandomAccessFile("file.txt", "rw");
raf.seek(0);
int b = raf.read();
raf.write(b + 1);
// NIO 의 해결: Channel (양방향)
try (FileChannel channel = FileChannel.open(
        Path.of("file.txt"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    channel.read(buffer);    // 같은 channel 로 읽기
    buffer.flip();
    channel.write(buffer);   // 같은 channel 로 쓰기
}

2.3 한계 3 — Blocking 의 동시성 문제

// IO 는 항상 Blocking
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept();   // ★ 연결 올 때까지 정지

InputStream in = client.getInputStream();
int b = in.read();   // ★ 데이터 올 때까지 정지

// 동시 처리하려면?
// 옵션: 스레드 per 연결
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
    Socket s = server.accept();
    pool.submit(() -> handleClient(s));
}
// 1만 연결 = 1만 스레드 = 큰 메모리 부담
// NIO 의 해결: Non-blocking + Selector
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));

Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();   // 한 스레드가 여러 채널 모니터
    // ... 한 스레드로 1만 연결 처리
}

2.4 한계 4 — Decorator 패턴의 복잡함

// 일반적 IO 사용 — 중첩 깊음
ObjectInputStream ois = new ObjectInputStream(
    new BufferedInputStream(
        new GZIPInputStream(
            new FileInputStream("data.dat.gz"))));

// 4중 중첩!
// 1. FileInputStream — 파일
// 2. GZIPInputStream — 압축 해제
// 3. BufferedInputStream — 버퍼링
// 4. ObjectInputStream — 객체 복원

// 가독성 ↓
// close 도 복잡 (가장 바깥만 close 하면 안쪽도 close 되지만)
// NIO.2 의 해결: 더 간결
String content = Files.readString(Path.of("file.txt"));   // 한 줄
List<String> lines = Files.readAllLines(Path.of("file.txt"));

try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.forEach(System.out::println);
}

2.5 한계 5 — File 클래스의 한계

// File 클래스의 한계
File file = new File("/path/to/file");

// 1. boolean 반환 — 진단 어려움
boolean created = file.createNewFile();   // 왜 실패?
boolean deleted = file.delete();          // 왜 실패?

// 2. 심볼릭 링크 처리 X
file.exists();   // 링크? 원본?

// 3. 풍부한 속성 X
file.canRead();
file.length();
file.lastModified();
// 끝 (POSIX 권한 등 없음)

// 4. 비효율적 디렉토리 순회
File[] children = dir.listFiles();   // 모두 메모리에

// 5. 파일 시스템 추상화 X
// ZIP, 메모리, 네트워크 파일 시스템 불가
// NIO.2 의 해결: Files + Path
Path path = Path.of("/path/to/file");

// 1. 명확한 예외
try {
    Files.createFile(path);
} catch (FileAlreadyExistsException e) { ... }
catch (AccessDeniedException e) { ... }

// 2. 심볼릭 링크 옵션
Files.exists(path, LinkOption.NOFOLLOW_LINKS);

// 3. 풍부한 속성
PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class);
Set<PosixFilePermission> perms = attrs.permissions();

// 4. Stream 기반 순회
try (Stream<Path> paths = Files.list(dir)) { ... }

// 5. 파일 시스템 추상화
FileSystem zipFs = FileSystems.newFileSystem(zipPath);

2.6 IO 의 5가지 한계 종합

IO (Java 1.0) 의 5가지 한계:

1. 1바이트씩 처리
   → 느림 (system call 빈번)
   → Buffered 로 일부 해결

2. 단방향
   → 양방향 작업 시 두 스트림 필요
   → NIO 의 Channel 이 해결

3. Blocking only
   → 동시성 ↓
   → 1만 연결 = 1만 스레드
   → NIO 의 Non-blocking 이 해결

4. Decorator 패턴 복잡
   → 중첩 깊음
   → NIO.2 의 Files 가 간결

5. File 의 한계
   → 진단 부실, 속성 부족
   → NIO.2 의 Path/Files 가 해결

2.7 자기 점검 답변

IO 의 5가지 한계와 해결책은?

:
1. 1바이트씩 → 느림: Buffered + byte[] 일부 해결
2. 단방향 → 양방향 작업 어려움: NIO Channel 해결
3. Blocking → 동시성 ↓: NIO Non-blocking 해결
4. Decorator 중첩 복잡: NIO.2 Files 간결
5. File 클래스 한계: NIO.2 Path/Files 해결

→ NIO + NIO.2 가 모두 해결.


3️⃣ NIO 의 등장과 핵심 개념 (Java 1.4)

3.1 NIO 의 등장

NIO (New I/O):
  - Java 1.4 (2002)
  - java.nio 패키지
  - IO 의 한계 해결 목표

핵심 변화:
  1. 스트림 → 채널 + 버퍼
  2. 단방향 → 양방향
  3. Blocking → Non-blocking 가능
  4. 1바이트 → 블록 단위

원동력:
  - 1990 년대 후반 인터넷 폭발
  - 대규모 서버 필요
  - 동시 연결 처리 요구

3.2 NIO 의 3가지 핵심

NIO 의 핵심 개념:

1. Channel (채널):
   - 양방향 데이터 통로
   - 파일, 소켓 등을 추상화
   - read/write 메서드

2. Buffer (버퍼):
   - 데이터의 컨테이너
   - 채널과 데이터 교환의 중간 매개
   - 위치 (position), 한계 (limit), 용량 (capacity)

3. Selector (셀렉터):
   - 여러 채널 모니터
   - 준비된 채널만 처리
   - 한 스레드로 다수 처리

3.3 Channel 의 정의

package java.nio.channels;

public interface Channel extends Closeable {
    boolean isOpen();
    void close() throws IOException;
}

// 양방향 채널
public interface ReadableByteChannel extends Channel {
    int read(ByteBuffer dst) throws IOException;
}

public interface WritableByteChannel extends Channel {
    int write(ByteBuffer src) throws IOException;
}

public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
    // 양방향
}

자바 표준 Channel 구현:

  • FileChannel: 파일
  • SocketChannel: TCP 클라이언트
  • ServerSocketChannel: TCP 서버
  • DatagramChannel: UDP
  • Pipe.SinkChannel/SourceChannel: 파이프

3.4 Buffer 의 구조

Buffer 의 4가지 속성:

capacity:  버퍼의 최대 크기 (불변)
position:  다음 읽기/쓰기 위치
limit:     읽기/쓰기 가능한 마지막 위치
mark:      position 의 임시 저장 (북마크)

조건:
  0 ≤ mark ≤ position ≤ limit ≤ capacity

상태:
  [0 .................. capacity]
   ↑position   ↑limit
   
쓰기 모드: position 이 다음 쓸 위치, limit = capacity
읽기 모드: position 이 다음 읽을 위치, limit = 쓴 데이터의 끝

3.5 Buffer 의 메서드

// 생성
ByteBuffer buffer = ByteBuffer.allocate(1024);   // heap
ByteBuffer direct = ByteBuffer.allocateDirect(1024);   // 직접 메모리

// 쓰기 (put)
buffer.put((byte) 65);
buffer.putInt(42);
buffer.putLong(123L);

// 읽기/쓰기 모드 전환
buffer.flip();      // 쓰기 → 읽기

// 읽기 (get)
byte b = buffer.get();
int n = buffer.getInt();
long l = buffer.getLong();

// 초기화
buffer.clear();     // 처음으로
buffer.rewind();    // position 만 0
buffer.compact();   // 안 읽은 데이터를 앞으로

// 정보
buffer.remaining();   // limit - position
buffer.hasRemaining();

3.6 Channel + Buffer 의 사용

// 파일 읽기 — NIO
try (FileChannel channel = FileChannel.open(Path.of("file.txt"))) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    
    int bytesRead = channel.read(buffer);
    while (bytesRead > 0) {
        buffer.flip();   // 쓰기 → 읽기 모드
        
        while (buffer.hasRemaining()) {
            System.out.print((char) buffer.get());
        }
        
        buffer.clear();   // 다시 쓰기 모드
        bytesRead = channel.read(buffer);
    }
}

3.7 Selector — 멀티플렉서

// 한 스레드가 여러 채널 모니터
Selector selector = Selector.open();

// 서버 채널 등록
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    selector.select();   // ★ 준비된 채널 대기 (Blocking, 하지만 한 번만)
    
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> it = keys.iterator();
    
    while (it.hasNext()) {
        SelectionKey key = it.next();
        it.remove();
        
        if (key.isAcceptable()) {
            // 새 연결
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            client.register(selector, SelectionKey.OP_READ);
        } else if (key.isReadable()) {
            // 데이터 도착
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            client.read(buffer);
            // 처리
        }
    }
}

// 한 스레드로 수천 연결 처리 가능

3.8 NIO 가 해결한 IO 의 한계

NIO 의 개선:

1. 블록 단위 (Buffer)
   → 1바이트 처리의 느림 해결

2. 양방향 (Channel)
   → 단방향의 불편함 해결

3. Non-blocking (configureBlocking(false))
   → Blocking 의 동시성 문제 해결

4. Selector
   → 한 스레드로 다수 처리

5. Direct Buffer
   → 직접 메모리 사용
   → JVM heap 우회
   → 네트워크 I/O 빠름

3.9 ILIC 활용 예

// NIO 로 파일 복사 — 고속
public class FastFileCopier {
    
    public void copy(Path src, Path dest) throws IOException {
        try (FileChannel srcChannel = FileChannel.open(src, StandardOpenOption.READ);
             FileChannel destChannel = FileChannel.open(dest,
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            // 채널 간 직접 전송 — 매우 빠름
            srcChannel.transferTo(0, srcChannel.size(), destChannel);
            // 또는: destChannel.transferFrom(srcChannel, 0, srcChannel.size());
        }
    }
}

// NIO 의 transferTo:
// - OS 의 zero-copy 활용 가능
// - 커널 ↔ 유저 공간 복사 회피
// - 일반 I/O 보다 수십% 빠름

3.10 자기 점검 답변

NIO 의 3가지 핵심 개념은?

:
1. Channel (채널):

  • 양방향 데이터 통로
  • FileChannel, SocketChannel 등
  • read/write 메서드
  1. Buffer (버퍼):

    • 데이터의 컨테이너
    • position, limit, capacity, mark
    • 쓰기/읽기 모드 전환 (flip)
  2. Selector (셀렉터):

    • 여러 채널 모니터
    • 한 스레드로 다수 처리
    • Non-blocking 의 핵심

→ Unit 7.3, 7.4 에서 정밀.


4️⃣ NIO.2 의 등장과 추가 기능 (Java 7)

4.1 NIO.2 의 등장

NIO.2:
  - Java 7 (2011)
  - java.nio.file 패키지
  - NIO 의 후속 (NIO 와는 다른 영역)

NIO 가 해결한 것:
  - 채널, 버퍼, Non-blocking (네트워크 위주)

NIO 가 못 해결한 것:
  - 파일 시스템 API (여전히 File 클래스)
  - 비동기 I/O (콜백 X)

NIO.2 가 해결:
  - Path, Files (현대 파일 API)
  - WatchService (변경 감지)
  - 비동기 채널
  - 파일 시스템 추상화

4.2 NIO.2 의 핵심 추가

NIO.2 의 추가 기능:

1. Path 인터페이스
   - 경로 객체 (불변)
   - File 대체

2. Files 정적 유틸리티
   - 모든 파일 작업
   - Stream 통합

3. WatchService
   - 파일 변경 감지

4. 비동기 채널
   - AsynchronousFileChannel
   - AsynchronousSocketChannel

5. 파일 시스템 추상화
   - FileSystem, FileSystems
   - ZIP, 메모리 FS 등

4.3 Path 와 Files

// 기본 사용
Path path = Path.of("/home/user/file.txt");
// 또는 Paths.get(...)

// Files 의 풍부한 메서드
Files.exists(path);
Files.isDirectory(path);
Files.isRegularFile(path);
Files.isReadable(path);

Files.readString(path);
Files.writeString(path, "content");
Files.readAllLines(path);
Files.write(path, lines);

Files.copy(src, dest);
Files.move(src, dest);
Files.delete(path);
Files.createFile(path);
Files.createDirectory(path);
Files.createDirectories(path);   // 중간 디렉토리도

// Stream 통합
try (Stream<String> lines = Files.lines(path)) {
    lines.forEach(System.out::println);
}

try (Stream<Path> paths = Files.list(dir)) { ... }
try (Stream<Path> paths = Files.walk(dir)) { ... }

4.4 WatchService — 파일 감시

// 파일/디렉토리 변경 감지
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    
    Path dir = Path.of("/path/to/watch");
    dir.register(watcher,
        StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_DELETE,
        StandardWatchEventKinds.ENTRY_MODIFY);
    
    while (true) {
        WatchKey key = watcher.take();   // 이벤트 대기
        
        for (WatchEvent<?> event : key.pollEvents()) {
            WatchEvent.Kind<?> kind = event.kind();
            Path changedFile = (Path) event.context();
            
            System.out.println(kind + ": " + changedFile);
        }
        
        if (!key.reset()) break;
    }
}

// 활용:
// - 설정 파일 hot reload
// - 로그 파일 분석
// - 파일 동기화

4.5 비동기 채널

// AsynchronousFileChannel — 비동기 파일
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("file.txt"), StandardOpenOption.READ);

ByteBuffer buffer = ByteBuffer.allocate(1024);

// 방법 1: Future
Future<Integer> future = channel.read(buffer, 0);
// 다른 일 가능
int bytesRead = future.get();   // 완료 대기

// 방법 2: CompletionHandler (콜백)
channel.read(buffer, 0, null, new CompletionHandler<Integer, Object>() {
    @Override
    public void completed(Integer result, Object attachment) {
        System.out.println("Read " + result + " bytes");
    }
    
    @Override
    public void failed(Throwable exc, Object attachment) {
        exc.printStackTrace();
    }
});

4.6 파일 시스템 추상화

// 기본 파일 시스템
FileSystem defaultFs = FileSystems.getDefault();

// ZIP 파일을 파일 시스템처럼
try (FileSystem zipFs = FileSystems.newFileSystem(Path.of("archive.zip"), null)) {
    
    Path inZip = zipFs.getPath("/inner-file.txt");
    String content = Files.readString(inZip);
    
    // ZIP 안의 파일을 일반 파일처럼 다룸
}

// 메모리 파일 시스템 (Google jimfs 같은 라이브러리)
// 네트워크 파일 시스템
// 사용자 정의 파일 시스템

// 동일한 API 로 모두 처리 — 추상화의 힘

4.7 NIO.2 의 효과

NIO.2 가 만든 변화:

1. 간결성
   - String content = Files.readString(path);
   - 한 줄로 끝

2. Stream 통합
   - Files.lines, list, walk
   - 함수형 프로그래밍 자연스러움

3. 명확한 예외
   - 구체적 예외 계층
   - 디버깅 용이

4. 풍부한 속성
   - 시간, 권한, 소유자
   - POSIX 지원

5. 추상화
   - ZIP, 메모리 FS 등
   - 동일 API

4.8 ILIC 활용

public class ShipmentFileService {
    
    private final Path baseDir = Path.of("/var/shipment");
    
    // NIO.2 의 간결한 파일 작업
    public List<Shipment> importFromCsv(Path src) throws IOException {
        try (Stream<String> lines = Files.lines(src)) {
            return lines
                .skip(1)   // 헤더 건너뛰기
                .map(this::parseShipment)
                .toList();
        }
    }
    
    public void exportToCsv(List<Shipment> shipments, Path dest) throws IOException {
        List<String> lines = new ArrayList<>();
        lines.add("id,blNo,weight,createdAt");
        shipments.forEach(s -> lines.add(s.toCsvLine()));
        Files.write(dest, lines);
    }
    
    public void copyToArchive(Path src) throws IOException {
        Path archive = baseDir.resolve("archive").resolve(src.getFileName());
        Files.createDirectories(archive.getParent());
        Files.copy(src, archive, StandardCopyOption.REPLACE_EXISTING);
    }
    
    public Stream<Path> findOldFiles(int days) throws IOException {
        Instant threshold = Instant.now().minus(days, ChronoUnit.DAYS);
        return Files.walk(baseDir)
            .filter(Files::isRegularFile)
            .filter(p -> {
                try {
                    return Files.getLastModifiedTime(p).toInstant().isBefore(threshold);
                } catch (IOException e) {
                    return false;
                }
            });
    }
}

4.9 자기 점검 답변

NIO.2 가 NIO 와 다른 점은?

:

  • NIO (Java 1.4):

    • 채널 + 버퍼
    • Non-blocking 가능
    • 네트워크 I/O 강화
    • 파일 API 여전히 File 클래스
  • NIO.2 (Java 7):

    • Path, Files (현대 파일 API)
    • Stream 통합
    • WatchService (변경 감지)
    • 비동기 채널
    • 파일 시스템 추상화

핵심: NIO.2 는 NIO 의 후속이지만 주로 파일 시스템 API 의 현대화. 자세히는 Unit 7.5 에서.


5️⃣ 3 시대 종합 비교

5.1 종합 비교 표

항목IO (Java 1.0, 1996)NIO (Java 1.4, 2002)NIO.2 (Java 7, 2011)
패키지java.iojava.nio, java.nio.channelsjava.nio.file
단위스트림 (1바이트/문자)채널 + 버퍼 (블록)채널 + 버퍼 + Path
방향단방향양방향양방향
Blocking항상Non-blocking 가능Non-blocking + Async
파일 APIFileFileChannelPath + Files (static)
Stream 통합XX✓ (Files.lines, walk)
멀티플렉싱XSelectorSelector + Async
비동기XXAsynchronousChannel
파일 시스템 추상화XX✓ (FileSystem)
변경 감지XX✓ (WatchService)
디렉토리 순회listFiles (메모리)(X)Stream 기반 (lazy)
가독성중첩 많음복잡 (Buffer 다룸)간결

5.2 시각적 비교

파일 읽기 — 같은 작업을 3가지 방식으로

IO (java.io):
  try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
      String line;
      while ((line = br.readLine()) != null) {
          // 처리
      }
  }
  
  특징:
  - Decorator 중첩
  - 한 줄씩 처리
  - Blocking

NIO (java.nio):
  try (FileChannel channel = FileChannel.open(Path.of("file.txt"))) {
      ByteBuffer buffer = ByteBuffer.allocate(1024);
      while (channel.read(buffer) > 0) {
          buffer.flip();
          // 처리
          buffer.clear();
      }
  }
  
  특징:
  - Buffer 관리
  - 블록 단위
  - 코드 복잡

NIO.2 (java.nio.file):
  try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
      lines.forEach(line -> /* 처리 */);
  }
  
  특징:
  - 한 줄
  - Stream 통합
  - 가독성 ↑

5.3 IO/NIO/NIO.2 공존

세 시대가 모두 살아있다.

이유:
  - 하위 호환성
  - 각각 적합한 시나리오
  - 라이브러리 의존성

예:
  - 직렬화: 여전히 java.io (ObjectInputStream)
  - 네트워크 (단순): java.io 가능
  - 네트워크 (대규모): java.nio 필수
  - 파일: NIO.2 권장
  - Spring Boot: NIO 기반 (Netty/Tomcat)

5.4 진화의 패턴

자바 I/O 진화의 일관된 패턴:

1. 한계 발견
   IO: 1바이트씩, Blocking
   ↓
2. 새 추상화 도입
   NIO: Channel + Buffer + Selector
   ↓
3. 부족한 부분 보강
   NIO.2: Path + Files + Async
   ↓
4. 하위 호환 유지
   - 옛 코드 계속 동작
   - 새 코드는 새 API

5.5 자바 I/O 의 역사 (정리)

1996: Java 1.0
  - java.io 등장
  - InputStream, OutputStream
  - Decorator 패턴
  
1997: Java 1.1
  - Reader, Writer 추가
  - 문자 스트림
  - 직렬화 (Serializable)
  
2002: Java 1.4
  - java.nio (NIO)
  - Channel, Buffer, Selector
  - Non-blocking 가능

2011: Java 7
  - java.nio.file (NIO.2)
  - Path, Files, WatchService
  - 비동기 채널
  - try-with-resources

2014: Java 8
  - Stream API 등장
  - Files.lines 의 강력함

2017: Java 9
  - try-with-resources effectively final
  - HttpClient (preview)

2018: Java 11
  - HttpClient 표준화
  - 단순한 네트워크 API

2021+: Java 17+
  - 점진적 개선
  - Project Loom (가상 스레드)
  - 비동기 패러다임 변화

5.6 자기 점검 답변

IO/NIO/NIO.2 의 4가지 핵심 차이는?

:
1. 단위:

  • IO: 스트림 (1바이트/문자)
  • NIO: 채널 + 버퍼 (블록)
  • NIO.2: 채널 + 버퍼 + Path
  1. 방향:

    • IO: 단방향
    • NIO/NIO.2: 양방향
  2. Blocking:

    • IO: 항상
    • NIO: Non-blocking 가능
    • NIO.2: Non-blocking + Async
  3. 파일 API:

    • IO: File
    • NIO: FileChannel
    • NIO.2: Path + Files (static)

6️⃣ File 클래스 vs Files 유틸리티

6.1 File 클래스 (Java 1.0)

// File 의 기본 사용
File file = new File("/path/to/file.txt");

// 1. 객체 생성 필요
// 2. 인스턴스 메서드들

file.exists();
file.length();
file.canRead();
file.canWrite();
file.lastModified();
file.getName();
file.getParent();
file.getPath();
file.getAbsolutePath();
file.getCanonicalPath();   // throws IOException

file.createNewFile();   // boolean 반환
file.delete();           // boolean 반환
file.renameTo(dest);     // boolean 반환
file.mkdirs();           // boolean 반환

file.listFiles();        // 디렉토리의 모든 파일 배열
file.list();             // 이름 배열

특징:

  • 객체 생성 후 사용
  • boolean 반환 (실패 원인 모름)
  • 제한된 기능

6.2 Files 유틸리티 (Java 7+)

// Files 의 사용
Path path = Path.of("/path/to/file.txt");

// 1. 객체 생성 불필요 (Path 만 있으면)
// 2. static 메서드들

// 조회
Files.exists(path);
Files.size(path);
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.isDirectory(path);
Files.isRegularFile(path);
Files.isSymbolicLink(path);
Files.isHidden(path);
Files.getLastModifiedTime(path);
Files.getOwner(path);
Files.getAttribute(path, "...");

// 생성/삭제 — 예외로 명확
Files.createFile(path);          // throws IOException
Files.delete(path);               // throws IOException
Files.move(src, dest);            // throws IOException
Files.copy(src, dest);            // throws IOException
Files.createDirectory(path);
Files.createDirectories(path);

// 읽기/쓰기 — 한 줄!
String content = Files.readString(path);
List<String> lines = Files.readAllLines(path);
byte[] bytes = Files.readAllBytes(path);

Files.writeString(path, "content");
Files.write(path, lines);
Files.write(path, bytes);

// Stream
try (Stream<String> lines = Files.lines(path)) { ... }
try (Stream<Path> paths = Files.list(dir)) { ... }
try (Stream<Path> paths = Files.walk(dir)) { ... }

6.3 결정적 차이

File 의 한계:
  - "객체 생성 후 사용"
  - 객체가 무거움 (실제 파일 정보 일부 캐시)
  - 메서드가 boolean 반환 → 진단 어려움
  - 풍부한 속성 X
  - 심볼릭 링크 처리 X

Files 의 장점:
  - "static 호출"
  - Path 만 있으면 됨 (가벼움)
  - 명확한 예외
  - 풍부한 속성 (BasicFileAttributes, PosixFileAttributes)
  - 심볼릭 링크 옵션 (LinkOption.NOFOLLOW_LINKS)

6.4 비교 표

항목FileFiles
Java 버전1.0+7+
패키지java.iojava.nio.file
사용 방식인스턴스화static 호출
에러 처리boolean명확한 예외
심볼릭 링크XLinkOption
StreamXFiles.lines/list/walk
속성기본만풍부 (POSIX 등)
비동기XAsynchronousChannel 가능
파일 시스템로컬만FileSystem 추상화

6.5 File 의 활용 패턴 (레거시)

// 전통 File 사용
File dir = new File("/var/data");

// 디렉토리 생성
if (!dir.exists()) {
    if (!dir.mkdirs()) {
        throw new RuntimeException("Cannot create dir");
        // 왜 실패했는지 모름!
    }
}

// 파일 순회
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt"));
// 모든 파일을 한 번에 메모리에 로드
// 디렉토리가 크면 OOM 위험

for (File file : files) {
    System.out.println(file.getName());
    if (file.length() > 1_000_000) {
        // 1MB 이상
    }
}

6.6 Files 의 활용 패턴 (현대)

// 현대 Files 사용
Path dir = Path.of("/var/data");

// 디렉토리 생성
Files.createDirectories(dir);
// 이미 존재하면 무시
// 실패 시 명확한 예외 (AccessDenied, IOException 등)

// 파일 순회 — Stream
try (Stream<Path> files = Files.list(dir)) {
    files.filter(p -> p.toString().endsWith(".txt"))
        .filter(p -> {
            try {
                return Files.size(p) > 1_000_000;
            } catch (IOException e) {
                return false;
            }
        })
        .forEach(p -> System.out.println(p.getFileName()));
}
// Lazy + 메모리 효율적

6.7 File ↔ Path 변환

// File → Path (자바 7+)
File file = new File("file.txt");
Path path = file.toPath();

// Path → File
Path path = Path.of("file.txt");
File file = path.toFile();

// 활용
// 레거시 라이브러리가 File 받음
public void legacyMethod(File f) { ... }

// 새 코드는 Path 권장
Path myPath = Path.of("/data");
legacyMethod(myPath.toFile());

6.8 ILIC 의 File → Files 마이그레이션

// 옛 코드 (File 기반)
public class OldExporter {
    
    public void export(String dirPath, String fileName) throws IOException {
        File dir = new File(dirPath);
        if (!dir.exists() && !dir.mkdirs()) {
            throw new IOException("Cannot create dir");
        }
        
        File file = new File(dir, fileName);
        if (!file.createNewFile() && !file.exists()) {
            throw new IOException("Cannot create file");
        }
        
        try (FileWriter writer = new FileWriter(file)) {
            writer.write("data");
        }
    }
}

// 새 코드 (Files 기반)
public class NewExporter {
    
    public void export(String dirPath, String fileName) throws IOException {
        Path dir = Path.of(dirPath);
        Files.createDirectories(dir);   // 명확한 예외
        
        Path file = dir.resolve(fileName);
        Files.writeString(file, "data");
        // 한 줄로 끝
    }
}

6.9 자기 점검 답변

File 과 Files 의 결정적 차이는?

:
1. 사용 방식:

  • File: 인스턴스화 (new File(...))
  • Files: static 호출 (Files.exists(path))
  1. 에러 처리:

    • File: boolean (왜 실패? 모름)
    • Files: 명확한 예외 (FileAlreadyExistsException 등)
  2. 기능:

    • File: 기본만 (length, lastModified)
    • Files: 풍부 (POSIX 권한, 심볼릭 링크, Stream 등)
  3. Stream 통합:

    • File: X
    • Files: Files.lines/list/walk
  4. 권장:

    • 새 코드: Files
    • 레거시 호환: File ↔ Path 변환

7️⃣ 단방향 vs 양방향, Blocking vs Non-blocking

7.1 단방향 (IO)

// java.io 의 스트림 — 단방향

// 읽기 전용
InputStream in = new FileInputStream("file.txt");
in.read();   // 가능
// in.write(...);   // 메서드 없음

// 쓰기 전용
OutputStream out = new FileOutputStream("file.txt");
out.write(65);
// out.read();   // 메서드 없음

// 양방향 작업 시 두 스트림 필요
InputStream in = new FileInputStream("file.txt");
OutputStream out = new FileOutputStream("file.txt");
// 두 개를 관리해야

7.2 양방향 (NIO)

// NIO 의 Channel — 양방향

try (FileChannel channel = FileChannel.open(
        Path.of("file.txt"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    
    // 같은 채널로 읽기
    ByteBuffer readBuf = ByteBuffer.allocate(1024);
    channel.read(readBuf);
    
    // 같은 채널로 쓰기
    ByteBuffer writeBuf = ByteBuffer.wrap("Hello".getBytes());
    channel.write(writeBuf);
}

// SocketChannel — 네트워크 양방향
SocketChannel socket = SocketChannel.open(new InetSocketAddress(host, port));
socket.read(buffer);    // 서버로부터 읽기
socket.write(buffer);   // 서버로 쓰기

7.3 단방향 vs 양방향 의 효과

단방향의 효과:
  + 단순한 API
  + 명확한 의도 (in 인지 out 인지)
  - 양방향 작업 시 객체 관리 ↑
  - 메모리 사용 ↑

양방향의 효과:
  + 효율적 (한 객체로 모두)
  + Random Access 자연스러움
  + 같은 채널로 read + write
  - API 복잡
  - 모드 관리 필요

7.4 Blocking 의 동작

// Blocking IO
ServerSocket server = new ServerSocket(8080);

Socket client = server.accept();   // ★ 클라이언트 연결 올 때까지 대기

InputStream in = client.getInputStream();
int b = in.read();   // ★ 데이터 올 때까지 대기

out.write(response.getBytes());   // ★ 모든 데이터 보낼 때까지 대기

// 한 스레드가 한 연결만 처리 가능
// 여러 연결 = 여러 스레드

7.5 Non-blocking 의 동작

// Non-blocking NIO
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);   // ★ Non-blocking 모드
server.bind(new InetSocketAddress(8080));

while (true) {
    SocketChannel client = server.accept();   // ★ 즉시 리턴 (없으면 null)
    if (client == null) {
        // 다른 일 가능
        continue;
    }
    
    client.configureBlocking(false);
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int n = client.read(buffer);   // ★ 즉시 리턴
    if (n == 0) {
        // 데이터 없음, 다른 일 가능
    } else if (n > 0) {
        // 데이터 있음, 처리
    }
}

// 또는 Selector 활용
// 한 스레드로 다수 연결 처리

7.6 Blocking vs Non-blocking 의 효과

Blocking:
  + 단순한 코드
  + 디버깅 쉬움
  - 동시성 ↓ (스레드 per 연결)
  - 메모리 ↑

Non-blocking:
  + 동시성 ↑ (한 스레드로 다수)
  + 메모리 효율
  - 코드 복잡
  - Selector 등 추가 추상화 필요
  - 디버깅 어려움

7.7 비교 시각화

시나리오: 1만 동시 연결 처리

Blocking:
  스레드 1만 개 필요
  - 각 스레드: 1MB 스택
  - 메모리: 10GB
  - 컨텍스트 스위칭 비용 ↑
  - CPU 캐시 효율 ↓

Non-blocking:
  스레드 8~16 개 (CPU 코어 수)
  - 한 스레드가 수천 연결 모니터
  - 메모리: 수십 MB
  - 효율적

7.8 ILIC 의 활용

// ILIC 의 Spring Boot
// 기본 Tomcat: NIO 기반 (Non-blocking)
// 또는 WebFlux: Reactor + Netty (완전 비동기)

@RestController
public class ApiController {
    
    @GetMapping("/api/shipments/{id}")
    public ShipmentResponse get(@PathVariable Long id) {
        // 동기 API 처럼 보이지만
        // 서블릿 컨테이너는 NIO 활용
        return service.findById(id);
    }
}

// WebFlux (완전 비동기)
@RestController
public class ReactiveController {
    
    @GetMapping("/api/shipments/{id}")
    public Mono<ShipmentResponse> get(@PathVariable Long id) {
        return service.findByIdAsync(id);
        // Non-blocking, Reactive
    }
}

7.9 자기 점검 답변

단방향/양방향, Blocking/Non-blocking 의 의미는?

:

  • 단방향 vs 양방향:

    • IO: 단방향 (InputStream OR OutputStream)
    • NIO: 양방향 (Channel)
    • 효과: 객체 관리, 메모리 효율
  • Blocking vs Non-blocking:

    • Blocking: 호출이 결과까지 정지
    • Non-blocking: 즉시 리턴
    • 동시성에 결정적

Unit 7.4 (마스터 깊이) 에서 정밀.


8️⃣ 실무 활용 가이드

8.1 언제 어느 것을 쓰나?

실무 선택 가이드:

NIO.2 (java.nio.file) 권장:
  ✓ 파일 작업 일반
  ✓ Stream 활용
  ✓ 새 코드

NIO (java.nio) 권장:
  ✓ 대규모 네트워크
  ✓ 직접 메모리 (DirectBuffer)
  ✓ zero-copy (transferTo)
  ✓ Custom 프로토콜 구현

IO (java.io) 권장:
  ✓ 직렬화 (ObjectStream)
  ✓ Reader/Writer (텍스트)
  ✓ 단순한 콘솔 I/O
  ✓ 레거시 호환

8.2 시나리오별 선택

시나리오 1: 파일 한 줄씩 처리

IO 방식 (옛):
  BufferedReader br = new BufferedReader(new FileReader("file.txt"));
  String line;
  while ((line = br.readLine()) != null) {
      // 처리
  }
  br.close();

NIO.2 방식 (권장):
  try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
      lines.forEach(line -> /* 처리 */);
  }
  // 더 간결, Stream API 활용
시나리오 2: 대용량 파일 복사

IO 방식 (옛):
  try (InputStream in = new BufferedInputStream(new FileInputStream(src));
       OutputStream out = new BufferedOutputStream(new FileOutputStream(dest))) {
      byte[] buf = new byte[8192];
      int n;
      while ((n = in.read(buf)) > 0) {
          out.write(buf, 0, n);
      }
  }

NIO 방식 (빠름):
  try (FileChannel srcCh = FileChannel.open(src, READ);
       FileChannel destCh = FileChannel.open(dest, WRITE, CREATE)) {
      srcCh.transferTo(0, srcCh.size(), destCh);
      // zero-copy 활용 (OS 지원 시)
  }

NIO.2 방식 (간결):
  Files.copy(src, dest);
  // 한 줄
시나리오 3: 대규모 동시 서버

IO 방식 (제한):
  - 1 연결 = 1 스레드
  - 1만 연결 = 1만 스레드
  - 메모리 부담

NIO 방식 (대규모):
  - Selector + Non-blocking
  - 한 스레드로 1만 연결
  - Netty 같은 라이브러리 권장

실무:
  - Spring WebFlux (Reactor + Netty)
  - 또는 Vert.x
  - 또는 Project Loom (Java 21+ 가상 스레드)
시나리오 4: 직렬화

여전히 IO 사용:
  ObjectOutputStream oos = new ObjectOutputStream(
      new FileOutputStream("data.dat"));
  oos.writeObject(shipment);

NIO.2 와 결합:
  try (OutputStream os = Files.newOutputStream(Path.of("data.dat"));
       ObjectOutputStream oos = new ObjectOutputStream(os)) {
      oos.writeObject(shipment);
  }
  // Files.newOutputStream 으로 시작, 안은 IO

직렬화는 Unit 9.4 에서 정밀

8.3 라이브러리 의존성

실무 라이브러리:

Apache Commons IO:
  - IOUtils.copy, IOUtils.toString
  - 편의 유틸리티
  - IO 시대의 산물이지만 여전히 유용

Guava (Google):
  - Files.toString (deprecated, Files.readString 권장)
  - ByteStreams, CharStreams

Spring:
  - FileSystemResource, ClassPathResource
  - 추상화 자원

Netty:
  - Non-blocking 네트워크의 표준
  - Reactor, gRPC 의 토대

8.4 운영 환경 고려

운영 시 고려할 점:

1. 파일 시스템 추상화 (NIO.2)
   - ZIP, 메모리, 네트워크 FS
   - 동일 코드로 다양한 FS

2. 비동기 I/O (NIO/NIO.2)
   - CompletableFuture 활용
   - 스레드 효율

3. 자원 관리
   - try-with-resources 필수
   - AutoCloseable 의 우아한 처리

4. 모니터링
   - I/O 시간 측정
   - 슬로우 작업 추적

5. 에러 처리
   - 명확한 예외 (NIO.2)
   - 재시도 로직

8.5 ILIC 의 종합 활용

@Service
public class ShipmentIoService {
    
    private final Path baseDir = Path.of("/var/shipment");
    
    // 1. NIO.2 — 일반 파일 작업
    public void exportCsv(List<Shipment> shipments, String filename) throws IOException {
        Path file = baseDir.resolve(filename);
        Files.createDirectories(file.getParent());
        
        try (BufferedWriter writer = Files.newBufferedWriter(file)) {
            writer.write("id,blNo,weight\n");
            for (Shipment s : shipments) {
                writer.write(s.toCsvLine());
                writer.newLine();
            }
        }
    }
    
    // 2. NIO — 고속 복사
    public void copyToBackup(Path src) throws IOException {
        Path backup = baseDir.resolve("backup").resolve(src.getFileName());
        Files.createDirectories(backup.getParent());
        
        try (FileChannel srcCh = FileChannel.open(src, StandardOpenOption.READ);
             FileChannel destCh = FileChannel.open(backup,
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            srcCh.transferTo(0, srcCh.size(), destCh);
        }
    }
    
    // 3. IO — 직렬화
    public void saveAsBinary(Shipment shipment, String name) throws IOException {
        Path file = baseDir.resolve(name);
        
        try (OutputStream os = Files.newOutputStream(file);
             ObjectOutputStream oos = new ObjectOutputStream(os)) {
            oos.writeObject(shipment);
        }
    }
    
    // 4. NIO.2 + Stream — 통계
    public long countLargeFiles(long thresholdBytes) throws IOException {
        try (Stream<Path> files = Files.walk(baseDir)) {
            return files
                .filter(Files::isRegularFile)
                .filter(p -> {
                    try {
                        return Files.size(p) > thresholdBytes;
                    } catch (IOException e) {
                        return false;
                    }
                })
                .count();
        }
    }
    
    // 5. NIO.2 + WatchService — 디렉토리 감시
    @PostConstruct
    public void watchDirectory() throws IOException {
        WatchService watcher = FileSystems.getDefault().newWatchService();
        baseDir.register(watcher,
            StandardWatchEventKinds.ENTRY_CREATE);
        
        new Thread(() -> {
            while (true) {
                try {
                    WatchKey key = watcher.take();
                    key.pollEvents().forEach(event -> {
                        Path path = (Path) event.context();
                        log.info("New file: {}", path);
                    });
                    if (!key.reset()) break;
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    return;
                }
            }
        }).start();
    }
}

8.6 자기 점검 답변

실무에서 IO/NIO/NIO.2 선택 기준은?

:
1. NIO.2 (java.nio.file) 기본:

  • 새 코드
  • 파일 작업
  • Stream 활용
  1. NIO (java.nio):

    • 대규모 네트워크
    • DirectBuffer
    • zero-copy
  2. IO (java.io):

    • 직렬화 (ObjectStream)
    • Reader/Writer 텍스트
    • 레거시 호환
  3. 결합:

    • NIO.2 의 newInputStream/newOutputStream 으로 IO 와 결합
    • 동시 활용 가능

→ 한 시스템에서 셋 모두 사용 가능.


9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
IO/NIO/NIO.2 차이?스트림 vs 채널+버퍼 vs Path+Files
등장 버전?1.0 / 1.4 / 7
IO 의 5가지 한계?1바이트, 단방향, Blocking, Decorator, File
Channel 의 효과?양방향, 블록 단위
Buffer 의 4속성?capacity, position, limit, mark
Selector?한 스레드로 다수 채널
File vs Files?인스턴스 vs static, 진단
NIO.2 의 추가?Path, Files, WatchService, Async
Stream 통합?Files.lines, walk
어느 것을 언제?파일 NIO.2, 네트워크 대규모 NIO, 직렬화 IO
Decorator 패턴?기본 + 보조 스트림 중첩

9.2 자기 점검 체크리스트

IO (java.io)

  • 스트림 기반 구조
  • 바이트 vs 문자 스트림
  • Decorator 패턴
  • 5가지 한계

NIO (java.nio)

  • Channel 의 정의
  • Buffer 의 구조
  • Selector 의 역할
  • zero-copy (transferTo)
  • DirectBuffer

NIO.2 (java.nio.file)

  • Path 와 Files
  • WatchService
  • AsynchronousChannel
  • FileSystem 추상화
  • Stream 통합

비교

  • 3 시대 차이
  • 단방향 vs 양방향
  • Blocking vs Non-blocking
  • File vs Files

실무

  • 선택 기준
  • 라이브러리 의존
  • 결합 활용
  • 모니터링

9.3 추가 심화 질문

Q1: NIO 가 무조건 더 빠른가?

답:

  • 아니다.
  • NIO 는 대규모 동시 연결 에서 유리
  • 단순한 단일 파일 작업은 IO/NIO.2 가 더 빠를 수 있음
  • 이유:
    • Buffer 관리 비용
    • 직접 메모리 할당/해제 비용
    • 작은 파일은 NIO 의 이점 적음

Q2: Direct Buffer 의 장단점?

답:

// Direct Buffer
ByteBuffer direct = ByteBuffer.allocateDirect(1024);

장점:
  - JVM heap 밖의 메모리
  - I/O 호출 시 복사 불필요 (zero-copy)
  - 네트워크 I/O 빠름

단점:
  - 할당/해제 비용 큼
  - GC 가 직접 정리 안 함 (Cleaner 사용)
  - 메모리 누수 가능
  - 적합한 용도: 큰 데이터, 자주 재사용

Q3: WatchService 의 한계?

답:

  • 운영체제 의존 (Linux inotify, macOS fsevents 등)
  • 일부 OS 는 폴링 사용 (느림)
  • 매우 빈번한 변경 시 이벤트 손실 가능
  • 큰 디렉토리에서 성능 저하

대안:

  • Apache Commons IO 의 FileAlterationMonitor
  • 사용자 정의 폴링

Q4: Files.lines 와 BufferedReader 의 차이?

답:

// Files.lines — Stream
try (Stream<String> lines = Files.lines(path)) {
    lines.forEach(System.out::println);
}

// BufferedReader — 전통
try (BufferedReader br = Files.newBufferedReader(path)) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
}

차이:
  - Files.lines: 함수형, Stream 활용
  - BufferedReader: 명령형, 전통
  - 성능: 비슷 (내부 동일)
  - 가독성: Files.lines 가 짧음
  - 처리: Stream API 활용 시 Files.lines

Q5: 비동기 I/O 가 항상 좋은가?

답:

  • 아니다.

  • 비동기는 다음 시나리오에 적합:

    • 대규모 동시 I/O
    • I/O bound 작업
    • 마이크로서비스
  • 부적합:

    • CPU bound 작업
    • 단순한 스크립트
    • 디버깅이 중요한 경우
    • 코드 가독성이 우선인 경우
  • 대안 (Java 21+):

    • Project Loom 의 가상 스레드
    • 동기 코드의 단순함 + 비동기의 효율

🎯 핵심 요약 — 3줄 정리

1. 자바 I/O 3 시대

  • IO (1.0): 스트림, 1바이트, 단방향, Blocking
  • NIO (1.4): 채널+버퍼, 양방향, Non-blocking
  • NIO.2 (7): Path+Files, Stream 통합, Async

2. 결정적 차이 4가지

  • 단위: 스트림 → 채널+버퍼
  • 방향: 단방향 → 양방향
  • 동기: Blocking → Non-blocking
  • 파일: File → Files (static)

3. 실무 선택

  • 파일 일반: NIO.2 (Files)
  • 대규모 네트워크: NIO (Channel + Selector)
  • 직렬화/레거시: IO
  • 셋 다 공존 + 결합 가능

📚 다음으로...

Unit 7.3 — Stream vs Channel

이번 Unit에서 IO/NIO 의 큰 그림을 봤다면, 다음은 Stream 과 Channel 의 정밀 비교.

  • Stream (IO) 의 정확한 구조
  • Channel (NIO) 의 양방향
  • Buffer 의 정밀 (position/limit/capacity)
  • 두 가지의 성능 차이
  • 언제 어느 것을

Phase 7 진행 상황

🚀 Phase 7 — I/O 시스템 큰 그림
  ✅ Unit 7.1 I/O 란 무엇인가
  ✅ Unit 7.2 IO vs NIO (역사적 진화) ← 여기
  ⏭ Unit 7.3 Stream vs Channel
  ⏭ Unit 7.4 Blocking vs Non-blocking (★ 마스터 깊이)
  ⏭ Unit 7.5 오버헤드와 File 객체

3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주)
✅ Phase 6 — 객체 비교 (6.1 ~ 6.4 완주)
🚀 Phase 7 — I/O 시스템 큰 그림 (2/5 진행)

총: 28/43 Unit 작성 (약 65%)
profile
Software Developer

0개의 댓글