F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
자바 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 시대의 진화 = 한계 → 해결.
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. 면접 + 자기 점검
java.io (Java 1.0, 1996):
자바 최초의 I/O API.
당시 표준 디자인:
- C 의 stdio 영향
- 스트림 기반 사고
- 단방향, 1바이트씩
- 모든 I/O 가 Blocking
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
바이트 스트림 (InputStream, OutputStream):
- 1바이트 단위
- 모든 데이터 (이미지, 음악, 텍스트)
- 인코딩 처리 X
문자 스트림 (Reader, Writer):
- 1문자 단위 (Java 의 char, 2바이트)
- 텍스트 전용
- 인코딩 처리 자동
변환:
InputStreamReader: InputStream → Reader
OutputStreamWriter: OutputStream → Writer
예:
Reader r = new InputStreamReader(System.in, "UTF-8");
// 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();
특징:
// 파일 읽기
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");
}
// 객체 직렬화 (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 에서 정밀.
IO 의 한계 (다음 섹션에서 정밀):
1. 1바이트씩 → 느림
2. 단방향 → InputStream/OutputStream 따로
3. Blocking only → 동시성 ↓
4. 중첩 패턴 → 가독성 ↓
5. File 의 한계 → Unit 7.5 에서 정밀
// 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 — 객체 복원
IO (java.io) 의 핵심 구조는?
답:
1. 스트림 기반:
단방향:
Blocking only:
Decorator 패턴:
두 가지 스트림:
// 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) 으로 일부 해결.
// 같은 파일을 읽고 쓰기 위해 두 스트림 필요
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 로 쓰기
}
// 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만 연결 처리
}
// 일반적 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);
}
// 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);
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 가 해결
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 가 모두 해결.
NIO (New I/O):
- Java 1.4 (2002)
- java.nio 패키지
- IO 의 한계 해결 목표
핵심 변화:
1. 스트림 → 채널 + 버퍼
2. 단방향 → 양방향
3. Blocking → Non-blocking 가능
4. 1바이트 → 블록 단위
원동력:
- 1990 년대 후반 인터넷 폭발
- 대규모 서버 필요
- 동시 연결 처리 요구
NIO 의 핵심 개념:
1. Channel (채널):
- 양방향 데이터 통로
- 파일, 소켓 등을 추상화
- read/write 메서드
2. Buffer (버퍼):
- 데이터의 컨테이너
- 채널과 데이터 교환의 중간 매개
- 위치 (position), 한계 (limit), 용량 (capacity)
3. Selector (셀렉터):
- 여러 채널 모니터
- 준비된 채널만 처리
- 한 스레드로 다수 처리
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 구현:
Buffer 의 4가지 속성:
capacity: 버퍼의 최대 크기 (불변)
position: 다음 읽기/쓰기 위치
limit: 읽기/쓰기 가능한 마지막 위치
mark: position 의 임시 저장 (북마크)
조건:
0 ≤ mark ≤ position ≤ limit ≤ capacity
상태:
[0 .................. capacity]
↑position ↑limit
쓰기 모드: position 이 다음 쓸 위치, limit = capacity
읽기 모드: position 이 다음 읽을 위치, limit = 쓴 데이터의 끝
// 생성
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();
// 파일 읽기 — 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);
}
}
// 한 스레드가 여러 채널 모니터
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);
// 처리
}
}
}
// 한 스레드로 수천 연결 처리 가능
NIO 의 개선:
1. 블록 단위 (Buffer)
→ 1바이트 처리의 느림 해결
2. 양방향 (Channel)
→ 단방향의 불편함 해결
3. Non-blocking (configureBlocking(false))
→ Blocking 의 동시성 문제 해결
4. Selector
→ 한 스레드로 다수 처리
5. Direct Buffer
→ 직접 메모리 사용
→ JVM heap 우회
→ 네트워크 I/O 빠름
// 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 보다 수십% 빠름
NIO 의 3가지 핵심 개념은?
답:
1. Channel (채널):
Buffer (버퍼):
Selector (셀렉터):
→ Unit 7.3, 7.4 에서 정밀.
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 (변경 감지)
- 비동기 채널
- 파일 시스템 추상화
NIO.2 의 추가 기능:
1. Path 인터페이스
- 경로 객체 (불변)
- File 대체
2. Files 정적 유틸리티
- 모든 파일 작업
- Stream 통합
3. WatchService
- 파일 변경 감지
4. 비동기 채널
- AsynchronousFileChannel
- AsynchronousSocketChannel
5. 파일 시스템 추상화
- FileSystem, FileSystems
- ZIP, 메모리 FS 등
// 기본 사용
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)) { ... }
// 파일/디렉토리 변경 감지
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
// - 로그 파일 분석
// - 파일 동기화
// 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();
}
});
// 기본 파일 시스템
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 로 모두 처리 — 추상화의 힘
NIO.2 가 만든 변화:
1. 간결성
- String content = Files.readString(path);
- 한 줄로 끝
2. Stream 통합
- Files.lines, list, walk
- 함수형 프로그래밍 자연스러움
3. 명확한 예외
- 구체적 예외 계층
- 디버깅 용이
4. 풍부한 속성
- 시간, 권한, 소유자
- POSIX 지원
5. 추상화
- ZIP, 메모리 FS 등
- 동일 API
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;
}
});
}
}
NIO.2 가 NIO 와 다른 점은?
답:
NIO (Java 1.4):
NIO.2 (Java 7):
핵심: NIO.2 는 NIO 의 후속이지만 주로 파일 시스템 API 의 현대화. 자세히는 Unit 7.5 에서.
| 항목 | IO (Java 1.0, 1996) | NIO (Java 1.4, 2002) | NIO.2 (Java 7, 2011) |
|---|---|---|---|
| 패키지 | java.io | java.nio, java.nio.channels | java.nio.file |
| 단위 | 스트림 (1바이트/문자) | 채널 + 버퍼 (블록) | 채널 + 버퍼 + Path |
| 방향 | 단방향 | 양방향 | 양방향 |
| Blocking | 항상 | Non-blocking 가능 | Non-blocking + Async |
| 파일 API | File | FileChannel | Path + Files (static) |
| Stream 통합 | X | X | ✓ (Files.lines, walk) |
| 멀티플렉싱 | X | Selector | Selector + Async |
| 비동기 | X | X | AsynchronousChannel |
| 파일 시스템 추상화 | X | X | ✓ (FileSystem) |
| 변경 감지 | X | X | ✓ (WatchService) |
| 디렉토리 순회 | listFiles (메모리) | (X) | Stream 기반 (lazy) |
| 가독성 | 중첩 많음 | 복잡 (Buffer 다룸) | 간결 |
파일 읽기 — 같은 작업을 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 통합
- 가독성 ↑
세 시대가 모두 살아있다.
이유:
- 하위 호환성
- 각각 적합한 시나리오
- 라이브러리 의존성
예:
- 직렬화: 여전히 java.io (ObjectInputStream)
- 네트워크 (단순): java.io 가능
- 네트워크 (대규모): java.nio 필수
- 파일: NIO.2 권장
- Spring Boot: NIO 기반 (Netty/Tomcat)
자바 I/O 진화의 일관된 패턴:
1. 한계 발견
IO: 1바이트씩, Blocking
↓
2. 새 추상화 도입
NIO: Channel + Buffer + Selector
↓
3. 부족한 부분 보강
NIO.2: Path + Files + Async
↓
4. 하위 호환 유지
- 옛 코드 계속 동작
- 새 코드는 새 API
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 (가상 스레드)
- 비동기 패러다임 변화
IO/NIO/NIO.2 의 4가지 핵심 차이는?
답:
1. 단위:
방향:
Blocking:
파일 API:
// 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(); // 이름 배열
특징:
// 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)) { ... }
File 의 한계:
- "객체 생성 후 사용"
- 객체가 무거움 (실제 파일 정보 일부 캐시)
- 메서드가 boolean 반환 → 진단 어려움
- 풍부한 속성 X
- 심볼릭 링크 처리 X
Files 의 장점:
- "static 호출"
- Path 만 있으면 됨 (가벼움)
- 명확한 예외
- 풍부한 속성 (BasicFileAttributes, PosixFileAttributes)
- 심볼릭 링크 옵션 (LinkOption.NOFOLLOW_LINKS)
| 항목 | File | Files |
|---|---|---|
| Java 버전 | 1.0+ | 7+ |
| 패키지 | java.io | java.nio.file |
| 사용 방식 | 인스턴스화 | static 호출 |
| 에러 처리 | boolean | 명확한 예외 |
| 심볼릭 링크 | X | LinkOption |
| Stream | X | Files.lines/list/walk |
| 속성 | 기본만 | 풍부 (POSIX 등) |
| 비동기 | X | AsynchronousChannel 가능 |
| 파일 시스템 | 로컬만 | FileSystem 추상화 |
// 전통 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 이상
}
}
// 현대 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 + 메모리 효율적
// 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());
// 옛 코드 (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");
// 한 줄로 끝
}
}
File 과 Files 의 결정적 차이는?
답:
1. 사용 방식:
new File(...))Files.exists(path))에러 처리:
기능:
Stream 통합:
권장:
// 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");
// 두 개를 관리해야
// 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); // 서버로 쓰기
단방향의 효과:
+ 단순한 API
+ 명확한 의도 (in 인지 out 인지)
- 양방향 작업 시 객체 관리 ↑
- 메모리 사용 ↑
양방향의 효과:
+ 효율적 (한 객체로 모두)
+ Random Access 자연스러움
+ 같은 채널로 read + write
- API 복잡
- 모드 관리 필요
// Blocking IO
ServerSocket server = new ServerSocket(8080);
Socket client = server.accept(); // ★ 클라이언트 연결 올 때까지 대기
InputStream in = client.getInputStream();
int b = in.read(); // ★ 데이터 올 때까지 대기
out.write(response.getBytes()); // ★ 모든 데이터 보낼 때까지 대기
// 한 스레드가 한 연결만 처리 가능
// 여러 연결 = 여러 스레드
// 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 활용
// 한 스레드로 다수 연결 처리
Blocking:
+ 단순한 코드
+ 디버깅 쉬움
- 동시성 ↓ (스레드 per 연결)
- 메모리 ↑
Non-blocking:
+ 동시성 ↑ (한 스레드로 다수)
+ 메모리 효율
- 코드 복잡
- Selector 등 추가 추상화 필요
- 디버깅 어려움
시나리오: 1만 동시 연결 처리
Blocking:
스레드 1만 개 필요
- 각 스레드: 1MB 스택
- 메모리: 10GB
- 컨텍스트 스위칭 비용 ↑
- CPU 캐시 효율 ↓
Non-blocking:
스레드 8~16 개 (CPU 코어 수)
- 한 스레드가 수천 연결 모니터
- 메모리: 수십 MB
- 효율적
// 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
}
}
단방향/양방향, Blocking/Non-blocking 의 의미는?
답:
단방향 vs 양방향:
Blocking vs Non-blocking:
→ Unit 7.4 (마스터 깊이) 에서 정밀.
실무 선택 가이드:
NIO.2 (java.nio.file) 권장:
✓ 파일 작업 일반
✓ Stream 활용
✓ 새 코드
NIO (java.nio) 권장:
✓ 대규모 네트워크
✓ 직접 메모리 (DirectBuffer)
✓ zero-copy (transferTo)
✓ Custom 프로토콜 구현
IO (java.io) 권장:
✓ 직렬화 (ObjectStream)
✓ Reader/Writer (텍스트)
✓ 단순한 콘솔 I/O
✓ 레거시 호환
시나리오 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 에서 정밀
실무 라이브러리:
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 의 토대
운영 시 고려할 점:
1. 파일 시스템 추상화 (NIO.2)
- ZIP, 메모리, 네트워크 FS
- 동일 코드로 다양한 FS
2. 비동기 I/O (NIO/NIO.2)
- CompletableFuture 활용
- 스레드 효율
3. 자원 관리
- try-with-resources 필수
- AutoCloseable 의 우아한 처리
4. 모니터링
- I/O 시간 측정
- 슬로우 작업 추적
5. 에러 처리
- 명확한 예외 (NIO.2)
- 재시도 로직
@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();
}
}
실무에서 IO/NIO/NIO.2 선택 기준은?
답:
1. NIO.2 (java.nio.file) 기본:
NIO (java.nio):
IO (java.io):
결합:
→ 한 시스템에서 셋 모두 사용 가능.
| 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 패턴? | 기본 + 보조 스트림 중첩 |
답:
답:
// Direct Buffer
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
장점:
- JVM heap 밖의 메모리
- I/O 호출 시 복사 불필요 (zero-copy)
- 네트워크 I/O 빠름
단점:
- 할당/해제 비용 큼
- GC 가 직접 정리 안 함 (Cleaner 사용)
- 메모리 누수 가능
- 적합한 용도: 큰 데이터, 자주 재사용
답:
대안:
답:
// 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
답:
아니다.
비동기는 다음 시나리오에 적합:
부적합:
대안 (Java 21+):
1. 자바 I/O 3 시대
2. 결정적 차이 4가지
3. 실무 선택
이번 Unit에서 IO/NIO 의 큰 그림을 봤다면, 다음은 Stream 과 Channel 의 정밀 비교.
🚀 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 객체
✅ 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%)