F-LAB JAVA · 1주차 · Phase 7 · 외부 세계와의 통신
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
capacity, limit, position, mark)와 그 관계는?flip(), clear(), compact()는 각각 언제 쓰는가?allocate vs allocateDirect의 차이는?NIO (Java 1.4+) =
Channel(양방향 통로) +Buffer(고정 메모리 창고) +Selector(다중 채널 감시자)
— Stream IO의 "1바이트씩 · 블로킹 · 단방향" 한계를 깨뜨려, 단일 스레드로 수천~수만 동시 연결을 처리할 수 있게 한다.
| 시대 | 모델 | 비유 |
|---|---|---|
| Stream IO (Java 1.0~) | 직원 N명, 각자 1명씩 담당 | 손님 1만 명 = 직원 1만 명 필요. 손님이 말 안 하면 직원은 멍하니 대기 |
| NIO (Java 1.4+) | 모든 창구에 알림 벨, 직원 1명이 벨 울린 창구만 응대 | 손님 1만 명 = 직원 몇 명으로 충분. 직원은 절대 멍 때리지 않음 |
직원 = 스레드, 창구 = Channel, 벨 = OS 이벤트, 직원 1명 = Selector를 든 1 스레드.
1. 탄생 배경 — Stream IO의 한계 (10K Problem)
2. NIO 4대 추상 — Channel · Buffer · Selector · Charset
3. Buffer 메커닉 — 4가지 상태 변수와 상태 전이
4. Channel 종류와 특기 — FileChannel · SocketChannel · Zero-Copy
5. Selector — 멀티플렉싱 — 1 스레드 다중 채널의 진실
6. ILIC 실무 코드 7가지 — Zero-Copy · MappedByteBuffer · FileLock
7. 흔한 실수 9가지 — flip 누락 · iterator 제거 · MappedBuffer 해제
8. NIO vs NIO.2 vs Netty — 어디서 멈출 것인가
9. 면접 질문 + 자기 점검
try (InputStream in = new FileInputStream("invoice.pdf")) {
int b;
while ((b = in.read()) != -1) { // 한 바이트씩
process(b);
}
}
read()는 1바이트만 읽는다. BufferedInputStream으로 감싸도 표면상의 인터페이스는 여전히 byte 단위.
디스크 IO는 본질적으로 블록 단위인데, 추상화가 어긋난다.
InputStream — 읽기만OutputStream — 쓰기만InputStream in = socket.getInputStream();
int b = in.read(); // ❗ 데이터 올 때까지 스레드 정지
read() 호출 시 데이터가 없으면 스레드는 완전히 멈춘다.
다른 일 못 하고, OS 입장에선 그냥 자고 있는 스레드.
웹 서버 시나리오: 클라이언트 10,000명 동시 연결.
Stream IO 모델 (블로킹):
연결 1만 개 → 스레드 1만 개 (Thread-per-Connection)
스레드 1개 메모리:
- Stack: 512KB ~ 1MB
- TCB(Thread Control Block): 추가 오버헤드
총 메모리: 5GB ~ 10GB (메모리만!)
컨텍스트 스위칭: CPU의 대부분이 스레드 전환에 소모
실제 작업 CPU: 거의 0%
문제의 핵심:
read()에서 멈춰서 자고 있음→ 불가능한 모델. 이게 1999년경부터 "C10K Problem"으로 불린 유명한 문제다.
세 가지 동시 해결:
| 한계 | 해결책 |
|---|---|
| 1바이트씩 | Buffer — 블록 단위 처리 |
| 단방향 | Channel — 양방향 통로 |
| 블로킹 | NON_BLOCKING + Selector — 1 스레드 다중 연결 |
이후 Java 7에서 NIO.2(Unit 7.2의 Files/Path)가 보완되며 사실상 완성됨.
Stream IO 모델:
InputStream ─── byte ───► Application
Application ─── byte ───► OutputStream
(한 방향, 1 바이트씩)
NIO 모델:
◄─── read ───
Channel ←─── Buffer ───► Application
─── write ──►
(양방향, 블록 단위)
여러 Channel을 ───► Selector ───► 1 Thread가 감시
📡 Channel (java.nio.channels.Channel)
↳ 데이터 통로. Stream을 대체.
↳ 양방향. Buffer를 통해서만 IO
📦 Buffer (java.nio.Buffer)
↳ 고정 크기 메모리 영역.
↳ Primitive 타입별: ByteBuffer · IntBuffer · CharBuffer ...
↳ Channel ↔ Buffer가 모든 IO의 단위
🎯 Selector (java.nio.channels.Selector)
↳ 여러 Channel을 한 스레드로 감시.
↳ "준비된 Channel만" 깨워줌
🔤 Charset (java.nio.charset.Charset)
↳ 문자 인코딩. byte ↔ char 변환.
↳ Encoder · Decoder 제공
이 Unit에서는 Channel · Buffer · Selector에 집중. Charset은 별도 학습 주제.
Buffer는 NIO의 가장 까다로운 부분. 한 번에 정확히 외울 것.
public abstract class Buffer {
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}
| 변수 | 의미 | 비유 |
|---|---|---|
| capacity | 버퍼의 전체 크기 (불변) | 양동이 용량 |
| limit | 읽기/쓰기 가능 한계 | 양동이의 물 높이 표시 |
| position | 현재 작업 위치 | 다음에 떠갈 위치 |
| mark | 임시 저장 위치 (reset() 대상) | 책갈피 |
불변식: 0 ≤ mark ≤ position ≤ limit ≤ capacity
┌─ 1) 생성 직후 ─────────────────────────────┐
│ ByteBuffer.allocate(8) │
│ │
│ [_ _ _ _ _ _ _ _] │
│ ↑ ↑ │
│ position=0 limit=8 = capacity │
│ │
│ → 쓰기 모드. 8 바이트 쓸 수 있음 │
└──────────────────────────────────────────┘
┌─ 2) 4 바이트 쓴 후 ────────────────────────┐
│ buf.put((byte)1).put((byte)2) │
│ .put((byte)3).put((byte)4); │
│ │
│ [1 2 3 4 _ _ _ _] │
│ ↑ ↑ │
│ pos=4 limit=8 │
│ │
│ → 더 쓸 수 있고, 아직 못 읽음 │
└──────────────────────────────────────────┘
┌─ 3) flip() 후 ────────────────────────────┐
│ buf.flip(); │
│ // limit = position; │
│ // position = 0; │
│ │
│ [1 2 3 4 _ _ _ _] │
│ ↑ ↑ │
│ pos=0 limit=4 │
│ │
│ → 읽기 모드. 0~3까지 읽을 수 있음 │
└──────────────────────────────────────────┘
┌─ 4) 2 바이트 읽은 후 ──────────────────────┐
│ buf.get(); buf.get(); │
│ │
│ [1 2 3 4 _ _ _ _] │
│ ↑ ↑ │
│ pos=2 limit=4 │
└──────────────────────────────────────────┘
┌─ 5a) clear() — 모든 것 리셋 ──────────────┐
│ buf.clear(); │
│ // position = 0; │
│ // limit = capacity; │
│ // (데이터는 그대로지만 덮어쓰기 가능) │
│ │
│ [1 2 3 4 _ _ _ _] ← 데이터는 남아있음 │
│ ↑ ↑ │
│ pos=0 limit=8 │
└──────────────────────────────────────────┘
┌─ 5b) compact() — 안 읽은 것 앞으로 ────────┐
│ // 위 4) 상태에서 compact() 호출 │
│ buf.compact(); │
│ // 안 읽은 3,4를 앞으로 옮기고 │
│ // pos = 2, limit = capacity │
│ │
│ [3 4 3 4 _ _ _ _] │
│ ↑ ↑ │
│ pos=2 limit=8 │
│ │
│ → 이어서 쓸 수 있음. 안 읽은 데이터 보존 │
└──────────────────────────────────────────┘
| 메서드 | 동작 | 언제 |
|---|---|---|
allocate(n) | Heap에 버퍼 생성 | 일반 |
allocateDirect(n) | OS 메모리 직접 할당 | 큰 IO · 빠름 |
put(b) | 한 바이트 쓰기 | 쓰기 |
get() | 한 바이트 읽기 | 읽기 |
flip() | limit=pos; pos=0 | 쓰기 → 읽기 |
clear() | pos=0; limit=cap | 처음부터 다시 쓸 때 |
compact() | 안 읽은 데이터 보존 + 쓰기 | 부분 처리 후 이어 쓸 때 |
rewind() | pos=0 (limit 유지) | 다시 읽을 때 |
mark() | 현재 pos 저장 | 책갈피 |
reset() | mark 위치로 pos 복원 | 책갈피 돌아가기 |
remaining() | limit - position | 남은 양 |
hasRemaining() | pos < limit | 루프 조건 |
flip()을 까먹으면? — NIO 최대 실수ByteBuffer buf = ByteBuffer.allocate(8);
buf.put((byte)1).put((byte)2).put((byte)3);
// ❌ flip 없이 바로 channel.write(buf)
// 채널은 pos(=3)부터 limit(=8)까지 쓰려고 함
// → 이미 쓴 3바이트는 무시되고, 의미 없는 5바이트가 나감
buf.flip(); // ✅ pos=0, limit=3
channel.write(buf); // 1, 2, 3 정확히 출력
암기 문장:
"쓰기 끝났으면 flip, 읽기 끝나면 clear 또는 compact"
allocate vs allocateDirectByteBuffer heapBuf = ByteBuffer.allocate(1024); // Heap 안
ByteBuffer directBuf = ByteBuffer.allocateDirect(1024); // OS 메모리 직접
| 항목 | allocate (Heap) | allocateDirect (Direct) |
|---|---|---|
| 위치 | JVM Heap | OS 메모리 (Heap 밖) |
| GC 대상 | ✓ | ✗ (Cleaner가 정리) |
| 할당 비용 | 낮음 | 높음 |
| IO 성능 | 보통 | 빠름 (커널 복사 1번 줄어듦) |
| 메모리 누수 위험 | 없음 | 있음 (Cleaner가 못 정리하면) |
언제 Direct?
언제 Heap?
→ 잘못된 선택의 함정: 작은 버퍼를 매번 allocateDirect 하면 할당 비용 때문에 오히려 느려진다.
📡 Channel
├── FileChannel 파일 IO
├── SocketChannel TCP 클라이언트
├── ServerSocketChannel TCP 서버 (accept)
├── DatagramChannel UDP
└── Pipe.SinkChannel / 스레드 간 통신
Pipe.SourceChannel
<try (FileChannel fc = FileChannel.open(path,
StandardOpenOption.READ)) {
ByteBuffer buf = ByteBuffer.allocate(1024);
while (fc.read(buf) > 0) { // Channel → Buffer
buf.flip();
process(buf);
buf.clear();
}
}
핵심 패턴:
1. read → Buffer 채움
2. flip → 읽기 모드
3. 처리
4. clear → 다음 read 준비
대용량 파일 전송 시 일반 IO의 문제:
일반 IO (4번 복사):
Disk
↓ (1) DMA
Kernel Buffer (page cache)
↓ (2) CPU 복사
User Space (Java byte[])
↓ (3) CPU 복사
Kernel Socket Buffer
↓ (4) DMA
Network
→ User Space를 거치는 2번의 CPU 복사가 낭비
Zero-Copy:
try (FileChannel fc = FileChannel.open(path, READ);
WritableByteChannel out = Channels.newChannel(outputStream)) {
fc.transferTo(0, fc.size(), out);
}
Zero-Copy (2번 복사):
Disk
↓ DMA
Kernel Buffer
↓ DMA (page cache → socket buffer, CPU 미관여)
Network
→ Linux의 sendfile() 시스템 콜 활용
→ CPU 사용량 ↓, 메모리 대역폭 ↓, 처리량 ↑
성능 차이: 큰 파일 전송 시 일반 IO 대비 2~5배 빠름.
파일을 메모리에 매핑.
try (FileChannel fc = FileChannel.open(huge, READ)) {
MappedByteBuffer buf = fc.map(
FileChannel.MapMode.READ_ONLY, 0, fc.size()
);
// 4GB 파일도 마치 byte[] 처럼 다룸
byte b = buf.get(1_000_000_000L); // ❌ 컴파일 에러 (int)
byte b = buf.get(0); // ✓ random access
buf.position(500_000_000);
buf.get(new byte[1024]);
}
MapMode:
READ_ONLY: 읽기만READ_WRITE: 쓰기 시 파일에 반영PRIVATE: 쓰기 시 사본 (copy-on-write)장점:
read() 호출 오버헤드 없음단점:
try (FileChannel fc = FileChannel.open(path, READ, WRITE);
FileLock lock = fc.lock()) { // exclusive lock
// 임계 영역 — 다른 JVM 프로세스가 못 읽음
processFile(fc);
}
// lock 자동 해제
모드:
lock() — exclusive (배타)lock(pos, size, shared) — 부분 잠금, shared 가능tryLock() — 즉시 시도, 못 잡으면 null용도 (ILIC):
try (SocketChannel sc = SocketChannel.open()) {
sc.connect(new InetSocketAddress("api.carrier.com", 443));
ByteBuffer buf = ByteBuffer.wrap("GET / HTTP/1.1\r\n\r\n".getBytes());
sc.write(buf);
ByteBuffer in = ByteBuffer.allocate(4096);
sc.read(in);
in.flip();
// ...
}
configureBlocking(false) 호출 시 논블로킹 모드 → Selector와 함께 사용.
Thread 1개
↓
Selector
├── Channel A (READ ready?)
├── Channel B (WRITE ready?)
├── Channel C (ACCEPT ready?)
└── ... N개 Channel
Thread는 selector.select()에서 대기
"준비된 Channel이 있을 때만" 깨어남
OS 시스템 콜로 수천 개의 fd(file descriptor)를 한 번에 감시할 수 있다는 사실에 기반.
| OS | 시스템 콜 | 특징 |
|---|---|---|
| Linux | epoll | 가장 효율적, fd 수 무제한 |
| macOS / BSD | kqueue | epoll과 유사 |
| Windows | IOCP / select | 동작 모델 다름 |
| 구식 (모든 OS) | select, poll | fd 수 제한 (보통 1024) |
Java NIO는 OS에 맞는 시스템 콜을 자동 선택.
SelectionKey.OP_ACCEPT // ServerSocketChannel: 새 연결 수락 가능
SelectionKey.OP_CONNECT // SocketChannel: 연결 완료
SelectionKey.OP_READ // 읽을 데이터 있음
SelectionKey.OP_WRITE // 쓸 수 있는 상태
public void runEchoServer(int port) throws IOException {
try (Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open()) {
server.bind(new InetSocketAddress(port));
server.configureBlocking(false); // ⚠ 필수
server.register(selector, SelectionKey.OP_ACCEPT);
while (!Thread.currentThread().isInterrupted()) {
selector.select(); // 준비된 채널 있을 때까지 대기
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // ⚠ 필수: 처리한 키는 제거
if (key.isAcceptable()) {
handleAccept(key, selector);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
}
}
private void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ,
ByteBuffer.allocate(1024)); // 키에 버퍼 attach
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
int n = client.read(buf);
if (n == -1) {
client.close();
return;
}
buf.flip();
client.write(buf);
buf.compact(); // 안 보낸 게 남았으면 보존
}
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
handle(key);
}
// ❌ 다음 select()도 같은 키들이 다시 들어옴
// → 무한 루프
// ✅ iterator로 돌면서 remove() 호출
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handle(key);
}
이게 NIO 초보자가 가장 많이 빠지는 함정.
| 메서드 | 동작 |
|---|---|
select() | 준비된 채널 있을 때까지 블록 |
select(timeout) | 최대 timeout ms 까지 대기 |
selectNow() | 즉시 반환 (논블로킹) |
wakeup() | 다른 스레드에서 select() 깨우기 |
국제종합물류 ILIC에서 NIO를 어디서 어떻게 쓰는가.
@Service
public class InvoiceDownloadService {
public void streamInvoice(Long invoiceId, OutputStream out) {
Path file = pathFor(invoiceId);
try (FileChannel fc = FileChannel.open(file, READ);
WritableByteChannel oc = Channels.newChannel(out)) {
long size = fc.size();
long pos = 0;
while (pos < size) {
long sent = fc.transferTo(pos, size - pos, oc);
if (sent <= 0) break;
pos += sent;
}
} catch (IOException e) {
throw new DownloadException("스트리밍 실패", e);
}
}
}
포인트:
transferTo는 한 번에 전송 보장 X → 루프로 감싸야 함sendfile() 시스템 콜 사용read/write 대비 2~5배 처리량public SettlementSummary parseLargeSettlement(Path file) {
try (FileChannel fc = FileChannel.open(file, READ)) {
if (fc.size() > Integer.MAX_VALUE) {
// 2GB 이상은 분할 매핑
return parseInChunks(fc);
}
MappedByteBuffer buf = fc.map(READ_ONLY, 0, fc.size());
return parseCSV(buf); // byte[] 처럼 다룸
} catch (IOException e) {
throw new SettlementParseException(e);
}
}
포인트:
map()은 int 크기 (2GB) 제한public void processSettlementBatch(Path lockFile) {
try (FileChannel fc = FileChannel.open(lockFile, CREATE, WRITE);
FileLock lock = fc.tryLock()) {
if (lock == null) {
log.warn("다른 인스턴스가 처리 중 — 스킵");
return;
}
// 임계 영역 — 같은 머신의 다른 JVM도 못 들어옴
settlementProcessor.runDaily();
} catch (IOException e) {
throw new SettlementException(e);
}
}
포인트:
@Scheduled 가 여러 인스턴스에서 동시 실행되는 사고 방지public Path downloadFromS3(String key, Path target) {
try (InputStream s3In = s3Client.getObject(bucket, key);
ReadableByteChannel src = Channels.newChannel(s3In);
FileChannel dst = FileChannel.open(target, CREATE, WRITE)) {
dst.transferFrom(src, 0, Long.MAX_VALUE);
return target;
} catch (IOException e) {
throw new S3DownloadException(key, e);
}
}
포인트:
transferFrom = transferTo의 반대 방향public void exportShipmentsToCSV(Path target, Stream<Shipment> shipments) {
try (FileChannel fc = FileChannel.open(target, CREATE, WRITE)) {
ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024); // 64KB
Charset utf8 = StandardCharsets.UTF_8;
writeLine(fc, buf, "id,blNo,origin,destination", utf8);
shipments.forEach(s -> {
String line = s.getId() + "," + s.getBlNo() + "," + s.getOrigin() + "," + s.getDestination();
writeLine(fc, buf, line, utf8);
});
// 마지막 flush
buf.flip();
if (buf.hasRemaining()) fc.write(buf);
} catch (IOException e) {
throw new ExportException(e);
}
}
private void writeLine(FileChannel fc, ByteBuffer buf, String line, Charset cs) throws IOException {
byte[] bytes = (line + "\n").getBytes(cs);
if (bytes.length > buf.remaining()) {
buf.flip();
fc.write(buf);
buf.clear();
}
buf.put(bytes);
}
포인트:
allocateDirect — 큰 IO에 적합private final HttpClient http = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build(); // 내부적으로 NIO + Selector 사용
public CompletableFuture<TrackingInfo> fetchTracking(String trackingNo) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/tracking/" + trackingNo))
.GET()
.build();
return http.sendAsync(req, BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenApply(this::parseTracking);
}
포인트:
Spring Boot 기본 내장 톰캣은 NIO Connector 사용 (org.apache.coyote.http11.Http11NioProtocol).
# application.yml
server:
tomcat:
threads:
max: 200 # worker thread 풀
min-spare: 10
accept-count: 100 # backlog
connection-timeout: 20000
max-connections: 8192 # 동시 처리 가능 연결 (NIO 덕분에 thread 200으로 8192 처리)
포인트:
max-connections >> threads.max — NIO의 핵심 가치// ❌
buf.put(data);
channel.write(buf); // pos부터 limit까지 — 의미 없는 데이터 전송
// ✅
buf.put(data);
buf.flip();
channel.write(buf);
// 부분만 읽고 다시 채워야 할 때
buf.flip();
int n = channel.write(buf); // 일부만 쓰임
buf.compact(); // ✓ 안 쓴 부분 보존, 이어서 채울 수 있음
// ❌ clear()를 쓰면 안 쓴 데이터 사라짐
// ❌ 같은 키가 계속 깨어남
for (SelectionKey key : selector.selectedKeys()) {
handle(key);
}
// ✅
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handle(key);
}
// ❌ 블로킹 모드인 채로 register → IllegalBlockingModeException
SocketChannel sc = SocketChannel.open();
sc.register(selector, OP_READ);
// ✅
sc.configureBlocking(false);
sc.register(selector, OP_READ);
// ❌ 작은 임시 버퍼를 Direct로
for (int i = 0; i < 1000000; i++) {
ByteBuffer buf = ByteBuffer.allocateDirect(64); // 매번 OS 호출
// ...
}
// ✅ 작은 건 Heap, 또는 재사용
ByteBuffer reusable = ByteBuffer.allocate(64);
for (int i = 0; i < 1000000; i++) {
reusable.clear();
// ...
}
// ❌ Buffer는 thread-safe 아님
class Server {
private final ByteBuffer shared = ByteBuffer.allocate(4096);
void handleRequest(SocketChannel ch) {
ch.read(shared); // 여러 스레드가 동시 호출 시 데이터 깨짐
}
}
// ✅ 연결마다 별도 버퍼 (key.attachment() 활용)
<MappedByteBuffer buf = fc.map(...);
// 파일을 닫아도 매핑은 살아있음
// Cleaner가 언제 호출될지 모름 → 디스크에 락이 남을 수 있음
// Java 9+: Unsafe로 강제 해제 가능 (권장 X)
// 가장 안전한 방법: try 블록 안에서 사용 끝나면 빠르게 GC 유도
buf = null;
System.gc(); // 최후의 수단
// ❌ OS마다 한 번에 보낼 수 있는 양이 다름
fc.transferTo(0, fc.size(), out);
// ✅ 루프
long size = fc.size();
long pos = 0;
while (pos < size) {
long sent = fc.transferTo(pos, size - pos, out);
if (sent <= 0) break;
pos += sent;
}
// ❌ CPU 100%
while (true) {
selector.selectNow();
processKeys();
}
// ✅ 블로킹 또는 timeout
while (true) {
selector.select(); // 또는 select(1000)
processKeys();
}
Netty / Reactor Netty ── 9999 명이 여기서 멈춤
▲
│
Java 11+ HttpClient ── 80%의 일반 개발자
Java NIO.2 (Files/Path) ── 파일 작업 표준
▲
│
Java NIO (Channel/Buffer) ── 원리 이해의 마지막 층
▲
│
Java IO (Stream) ── 더는 안 쓰는 게 좋음
| 작업 | 권장 |
|---|---|
| 파일 작업 | NIO.2 (Files/Path) |
| HTTP 호출 | Java 11+ HttpClient |
| 대용량 파일 전송 | NIO FileChannel.transferTo |
| 메모리 매핑 | NIO MappedByteBuffer |
| 분산 락 (한 머신) | NIO FileLock |
| 고성능 서버 (1만+ 연결) | Netty (NIO 직접 사용 X) |
NIO를 직접 쓰면:
Selector 루프, key 관리, buffer 관리 → 코드 1000줄+Netty는 이걸 다 추상화. NIO의 원리를 이해하는 게 목표지, 직접 구현하는 게 목표가 아니다.
Spring Boot의 내장 톰캣, Netty Reactor (Spring WebFlux), gRPC Java 모두 내부에서 NIO 사용.
우리는 그 위에서 일하지만, 튜닝·문제 진단 시 원리가 필요.
| Q | 핵심 답변 |
|---|---|
| 전통 IO vs NIO의 결정적 차이? | Stream(단방향·1바이트·블로킹) vs Channel+Buffer(양방향·블록·논블로킹) |
| Buffer의 4가지 상태 변수? | capacity, limit, position, mark |
flip()은 정확히 뭘 하나? | limit = position; position = 0; mark = -1; |
clear() vs compact()? | clear: 전체 리셋 (데이터 무시), compact: 안 읽은 것 앞으로 보존 |
allocate vs allocateDirect? | Heap vs OS 메모리. Direct는 IO 빠르지만 할당 비쌈 |
| Zero-Copy가 뭐고 왜 빠른가? | User space 복사 생략, sendfile 시스템 콜. CPU·메모리 대역폭 절약 |
| Selector는 어떻게 동작? | OS 시스템 콜(epoll/kqueue/IOCP)로 다중 fd 감시 |
| 1만 동시 연결, 스레드 몇 개? | NIO + Selector 사용 시 수십 개로도 가능 |
| MappedByteBuffer 장단점? | 페이지 캐시 활용 · random access. 해제 어려움 · OS 의존 |
| NIO 직접 쓸 일이 있나? | 일반적으로 X. Netty/HttpClient/내장 톰캣이 알아서 사용 |
transferTo로 Zero-Copy 다운로드를 구현할 수 있다1. NIO = Stream IO의 3가지 한계를 동시 해결
2. Buffer 메커닉이 NIO의 진입 장벽
flip 누락이 가장 흔한 버그3. ILIC 실무에서 직접 쓸 곳은 한정적
transferTo (Zero-Copy)MappedByteBufferFileLockserialVersionUID 가 없으면 언제 터지는가transient로 보안 위험 차단 (비밀번호, 토큰, 캐시)