F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
🎯 마스터 프롬프트 깊이 Unit
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
Blocking I/O 는 "스레드 = 연결" 의 1:1 모델, Non-blocking I/O 는 "스레드 N 개로 연결 M 개 (N ≪ M)" 의 멀티플렉싱 모델이다.
Blocking 의read()는 OS 의 시스템 호출에서 스레드를 TASK_UNINTERRUPTIBLE 상태로 만들어 자바의Thread.interrupt()도 못 깨운다 — 빠져나오는 유일한 방법은 stream 의 close().
Non-blocking 은 OS 의select/poll/epoll시스템 호출과 Selector 의 결합으로 한 스레드가 수많은 채널을 모니터.
1만 동시 연결을 Blocking 으로 처리하려면 1만 스레드 = 10GB 메모리, Non-blocking 은 8~16 스레드 = 수십 MB — 이것이 C10K 문제 의 해법.
단, Non-blocking 이 만능은 아니다 — CPU bound 작업에선 Blocking + 멀티스레드가 더 효율적, 가독성도 좋다.
Blocking + 1:1 (전통 식당):
한 손님당 한 직원이 시중
- 음식 나올 때까지 직원이 그 테이블에 묶임
- 100 테이블 = 100 직원
- 일부 직원은 그냥 대기 중
- 인건비 ↑
Non-blocking + Selector (현대 푸드코트):
한 직원이 여러 테이블 모니터
- 테이블 호출 벨 → 준비된 곳만 응답
- 한 직원이 100 테이블 가능
- 효율적
- 단, 동시에 한 테이블만 응대 가능
Async (배달):
주문 후 손님이 다른 일
- 음식 완료 시 알림
- 가장 효율적
- 코드 복잡 (콜백 지옥)
Project Loom (가상 직원):
한 손님당 한 가상 직원
- 가상 직원은 거의 무료
- 1만 손님 = 1만 가상 직원
- 진짜 직원 (OS 스레드) 은 적게
- 가독성 ↑
→ I/O 모델 = 동시성과 자원의 트레이드오프.
1. Blocking I/O 의 정확한 동작
2. Blocking 의 5가지 구조적 문제
3. Non-blocking I/O 의 메커니즘
4. Selector — 멀티플렉서의 정밀
5. Sync vs Async 의 정확한 차이
6. 동시성 모델 비교 (스레드/이벤트/Reactive/Loom)
7. 1만 동시 연결 시나리오
8. 실무 적용 (Tomcat/Netty/WebFlux/Loom)
9. Phase 7 졸업 시험 + 완주
Blocking I/O:
I/O 함수 호출 → 작업 완료까지 호출자 (스레드) 정지.
완료 = "사용 가능한 데이터 있음" 또는 "쓰기 가능"
정지 동안:
- 스레드는 다른 일 못 함
- 다른 데이터 처리 X
- 인터럽트도 거의 안 통함
사용자 코드:
int b = inputStream.read();
JVM 의 동작:
1. native 메서드 호출
2. JNI 통해 OS 시스템 호출 (read syscall)
3. user space → kernel space 전환
Kernel 의 동작:
1. 파일 디스크립터 확인
2. 데이터 사용 가능?
- YES: kernel buffer → user buffer 복사 → 리턴
- NO: 스레드 상태 변경 (Running → TASK_UNINTERRUPTIBLE 또는 TASK_INTERRUPTIBLE)
wait queue 에 등록
스케줄러가 다른 스레드 실행
데이터 도착:
1. 디바이스 드라이버 인터럽트
2. kernel 이 wait queue 의 스레드 깨움
3. 스레드 상태 → Running
4. kernel buffer → user buffer 복사
5. user space 로 리턴
핵심:
- 스레드가 정지 = OS 가 스케줄러에서 제외
- 깨우는 조건 = 데이터 도착
Linux 의 프로세스/스레드 상태:
TASK_RUNNING (R):
- 실행 중 또는 실행 대기
TASK_INTERRUPTIBLE (S):
- I/O 등 대기 중
- 시그널 받으면 깨어남
- 일반 sleep, select 등
TASK_UNINTERRUPTIBLE (D):
- 깊은 I/O 대기 중 (디스크 등)
- 시그널 무시
- "uninterruptible sleep"
- 자바의 Thread.interrupt 도 못 깨움 (D 상태일 때)
TASK_ZOMBIE (Z):
- 종료됐지만 부모가 wait 안 함
TASK_STOPPED (T):
- 일시 정지 (SIGSTOP)
자바 Blocking I/O 의 함정:
- 일부 I/O 는 D 상태로 들어감
- thread.interrupt() 호출해도 안 풀림
- 빠져나오는 방법: 스트림 close() 만
// Blocking read 의 동작
public class BlockingReadDemo {
public static void main(String[] args) throws IOException {
// 표준 입력 (키보드)
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 이 줄에서 사용자가 입력할 때까지 정지
System.out.println("Waiting for input...");
String line = br.readLine(); // ★ Blocking
// 위 줄에서 영원히 멈출 수 있음
System.out.println("Got: " + line);
}
}
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try {
InputStream in = System.in;
int b = in.read(); // ★ Blocking
// interrupt 가 와도 read 는 계속됨
System.out.println("Got: " + b);
} catch (IOException e) {
System.out.println("IOException: " + e);
}
});
t.start();
Thread.sleep(1000);
// 1초 후 interrupt 시도
t.interrupt(); // ★ 안 통함!
Thread.sleep(2000);
if (t.isAlive()) {
System.out.println("Thread still alive!");
// 빠져나오는 유일한 방법: System.in 을 close
// 하지만 System.in 은 보통 못 close
}
}
}
// 결과:
// Thread still alive!
// → interrupt 는 일반 자바 코드 (sleep, wait, join) 만 깨움
// → Blocking I/O 는 못 깨움
// 1. 스트림 close()
// 가장 일반적 방법
public class CloseToInterruptDemo {
public static void main(String[] args) throws InterruptedException {
ServerSocket server = new ServerSocket(8080);
Thread t = new Thread(() -> {
try {
Socket client = server.accept(); // ★ Blocking
} catch (IOException e) {
System.out.println("Interrupted by close: " + e);
}
});
t.start();
Thread.sleep(1000);
// close() 가 accept() 를 깨움
server.close(); // ★ accept() 가 SocketException 던짐
t.join();
}
}
// 2. SO_TIMEOUT 설정 (Socket)
Socket socket = new Socket(host, port);
socket.setSoTimeout(5000); // 5초
try {
int b = socket.getInputStream().read();
} catch (SocketTimeoutException e) {
// 5초 후 자동 timeout
}
// 3. NIO 의 InterruptibleChannel
// FileChannel, SocketChannel 등
// thread.interrupt() 가 ClosedByInterruptException 던짐
try {
FileChannel ch = FileChannel.open(path);
ch.read(buffer); // Blocking
} catch (ClosedByInterruptException e) {
// interrupt 로 인한 종료
}
Blocking 모델의 자원:
연결 1 = 스레드 1
스레드의 자원:
- 스택 메모리 (기본 1MB, JVM 옵션 -Xss)
- 커널 자원 (스케줄러 관리)
- 컨텍스트 스위칭 비용 (~1μs)
- TLB cache miss 비용
100 연결:
- 메모리: 100MB
- OK
1,000 연결:
- 메모리: 1GB
- 한계 시작
10,000 연결:
- 메모리: 10GB
- 컨텍스트 스위칭 폭발
- 비현실적
// Blocking 코드는 단순
public class BlockingServer {
public void start() throws IOException {
ServerSocket server = new ServerSocket(8080);
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket client = server.accept(); // 연결 대기
pool.submit(() -> handleClient(client));
}
}
private void handleClient(Socket client) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter writer = new PrintWriter(
client.getOutputStream(), true)) {
String request = reader.readLine(); // Blocking
String response = process(request); // 일반 처리
writer.println(response); // Blocking
} catch (IOException e) {
// 처리
}
}
}
// 장점:
// - 직관적 흐름
// - 디버깅 쉬움
// - 가독성 ↑
// - 작은 서버에 충분
// ILIC 의 Spring MVC (Tomcat 기본)
@RestController
public class ShipmentController {
@GetMapping("/api/shipments/{id}")
public ShipmentResponse get(@PathVariable Long id) {
// 이 메서드는 Blocking
// 1. DB 조회 — Blocking
Shipment s = service.findById(id);
// 2. 외부 API — Blocking
TrackingInfo tracking = trackingApi.fetch(id);
// 3. 응답 — Blocking
return ShipmentResponse.from(s, tracking);
}
}
// Tomcat 의 기본 동작:
// - 요청당 1 스레드 (스레드 풀에서)
// - 기본 200 스레드 (server.tomcat.threads.max)
// - 동시 처리 가능한 요청 = 200
// - 그 이상은 큐에 대기
Blocking I/O 에서 스레드는 어떤 상태가 되나?
답:
1. OS 레벨:
자바 레벨:
빠져나오는 방법:
interrupt 가 안 통하는 이유:
연결 = 스레드 1:1 매핑
문제:
100 연결 → 100 스레드 (OK)
1,000 연결 → 1,000 스레드 (한계)
10,000 연결 → 10,000 스레드 (불가능)
원인:
스레드 비용:
- 메모리 (1MB 스택)
- 커널 자원 (PCB)
- 컨텍스트 스위칭
해결:
- 스레드 풀로 일부 완화
- Non-blocking 으로 근본 해결
컨텍스트 스위칭:
- CPU 가 스레드 A 에서 B 로 전환
- 레지스터 저장/복원
- TLB (Translation Lookaside Buffer) 무효화
- CPU 캐시 invalidation
비용:
- 1~10 마이크로초 (μs)
- 작아 보이지만 자주 일어나면 누적
1만 스레드 시:
- 초당 수만 번 스위칭
- CPU 시간의 큰 부분이 스위칭에
- 실제 작업 시간 ↓
스레드의 메모리:
- 스택 (기본 1MB)
- TLS (Thread Local Storage)
- 커널 데이터
1만 스레드:
- 스택만 10GB
- + 커널 데이터 = 더
- 64GB 서버도 부담
- 16GB 서버는 불가
// 일반 코드의 interrupt
public class NormalCode {
public void run() {
try {
Thread.sleep(10000); // 10초
} catch (InterruptedException e) {
// ★ interrupt() 호출하면 즉시 깨어남
}
}
}
// Blocking I/O 의 interrupt
public class BlockingIo {
public void run() {
InputStream in = System.in;
try {
int b = in.read(); // Blocking
// ★ interrupt() 호출해도 안 깨어남
// System.in 을 close 해야만 깨어남
} catch (IOException e) { ... }
}
}
이유:
Blocking 의 디버깅 어려움:
1. 스레드 덤프
- 1만 스레드의 스택 트레이스
- 분석 매우 어려움
2. 누가 막혔나?
- 어떤 연결이 응답 없음?
- 추적 복잡
3. 메모리 모니터링
- 1만 스레드의 메모리 사용
- GC 영향
4. 재시작
- 모든 스레드 정리 필요
- 시간 ↑
// 스레드 풀로 일부 완화
ExecutorService pool = Executors.newFixedThreadPool(200);
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket client = server.accept();
pool.submit(() -> handleClient(client));
}
// 한계:
// - 풀 크기 = 동시 처리 가능 연결 수
// - 200 으로 제한 → 100,001번째 요청은 큐 대기
// - 응답 시간 ↑
// - 큐가 가득 차면 거부
// 1만 동시 요청 → 9,800개가 대기
시나리오: 100 동시 연결, 각 요청이 1초 (I/O 대기 990ms + CPU 10ms)
Blocking + 100 스레드:
- 100 스레드가 동시에 실행
- CPU 사용률: 1% (대부분 대기)
- 메모리: 100MB (스택)
- 처리율: 100 req/sec
Non-blocking + 1 스레드:
- 1 스레드가 100 연결 모니터
- CPU 사용률: ~1% (대기는 OS 가 처리)
- 메모리: 수 MB
- 처리율: 100 req/sec (동일)
차이:
- 자원 사용 1/100
- 같은 처리량
- 메모리 효율 압도적
ILIC 시나리오:
평소: 100 동시 요청
- Blocking + Tomcat 200 스레드: OK
피크: 1,000 동시 요청
- Tomcat 200 스레드: 800 대기
- 응답 시간 ↑
- 일부 타임아웃
블랙 프라이데이: 10,000 동시 요청
- 시스템 마비
- Non-blocking 또는 수평 확장 필요
Blocking 의 5가지 구조적 문제는?
답:
1. 스레드 폭증: 1만 연결 = 1만 스레드
2. 컨텍스트 스위칭 비용: μs 단위, 누적 ↑
3. 메모리 사용: 1MB × 1만 = 10GB
4. Interrupt 한계: 일부 I/O 는 interrupt 안 통함
5. 디버깅 어려움: 1만 스레드 분석
해결:
Non-blocking I/O:
I/O 함수 호출 → 즉시 리턴.
- 데이터 있으면 가져옴
- 데이터 없으면 0 반환 (또는 즉시 알림)
- 스레드 정지 X
원리:
OS 의 시스템 호출이 이미 비차단 모드 지원
- read(fd, ..., O_NONBLOCK)
- 데이터 없으면 EAGAIN 에러
- 자바는 0 반환으로 추상화
// SocketChannel 을 Non-blocking 으로
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // ★ Non-blocking 모드
// connect
channel.connect(new InetSocketAddress(host, port));
// 즉시 리턴 (실제 연결은 아직)
// 완료 확인은 finishConnect()
while (!channel.finishConnect()) {
// 다른 일
}
// read
ByteBuffer buffer = ByteBuffer.allocate(1024);
int n = channel.read(buffer); // 즉시 리턴
if (n == 0) {
// 데이터 없음
} else if (n > 0) {
// 데이터 있음, n 바이트
} else if (n == -1) {
// 연결 종료
}
// write
int written = channel.write(buffer); // 즉시 리턴
// 일부만 쓸 수도 있음 (커널 버퍼 가득)
// 단순 Non-blocking — 폴링 (Polling)
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(...);
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
int n = channel.read(buffer); // ★ 계속 호출
if (n > 0) {
// 데이터 처리
break;
}
// n == 0: 데이터 없음
// 다시 시도
}
// 문제:
// - CPU 100% (계속 read 호출)
// - 비효율
// - 좋지 않은 패턴
멀티플렉싱 시스템 호출의 진화:
select (1983):
- 가장 오래된
- fd_set 비트맵
- 1024 파일 디스크립터 제한
- O(n) 검사
poll (1986):
- select 의 개선
- 1024 제한 없음
- 여전히 O(n)
epoll (Linux 2.6, 2003):
- 이벤트 기반
- 등록한 fd 만 모니터
- O(1) 검사
- 자바 NIO 의 기본 (Linux)
kqueue (BSD, macOS):
- BSD 계열의 epoll 등가물
IOCP (Windows):
- Windows 의 비동기 I/O
- I/O Completion Port
epoll 의 3가지 시스템 호출:
1. epoll_create:
- epoll 인스턴스 생성
- 커널의 데이터 구조 (Red-Black Tree)
2. epoll_ctl:
- fd 추가/수정/삭제
- 모니터링 이벤트 등록 (read, write 등)
3. epoll_wait:
- 이벤트 발생까지 대기
- 또는 timeout
- 발생한 이벤트만 반환
장점:
- 한 번 등록, 여러 번 wait
- 발생한 이벤트만 (O(1))
- 효율적
// Selector 는 epoll/kqueue/IOCP 의 자바 추상화
Selector selector = Selector.open();
// 내부: epoll_create
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);
// 내부: epoll_ctl
while (true) {
int n = selector.select(); // 이벤트 대기
// 내부: epoll_wait
if (n > 0) {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
}
}
}
Blocking read:
사용자: read(fd, buf, size)
커널: 데이터 없음 → 스레드 sleep
... (대기) ...
커널: 데이터 도착 → 스레드 깨움
커널: 데이터 복사 → 리턴
스레드: 그동안 정지
Non-blocking read:
사용자: read(fd, buf, size) # fd 가 O_NONBLOCK
커널: 데이터 없음 → EAGAIN
사용자: 다른 일
Non-blocking + epoll:
사용자: epoll_wait(epfd, events, ...)
커널: 등록된 fd 중 이벤트 있는 것 반환
사용자: read(fd) → 즉시 데이터 받음
한 스레드가 다수 fd 모니터
// 실무에선 직접 Selector 잘 안 씀 (Netty 등 라이브러리)
// 학습 목적 코드
public class NonBlockingShipmentServer {
private Selector selector;
public void start() throws IOException {
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(); // 이벤트 대기
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
accept(key);
} else if (key.isReadable()) {
read(key);
}
}
}
}
private void accept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
// 새 연결을 selector 에 등록
}
private void read(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int n = client.read(buffer);
if (n == -1) {
client.close();
return;
}
// 데이터 처리
buffer.flip();
String request = StandardCharsets.UTF_8.decode(buffer).toString();
// ILIC 요청 처리 (간단히)
if (request.contains("/api/shipments")) {
String response = "HTTP/1.1 200 OK\r\n\r\n[shipment data]";
client.write(ByteBuffer.wrap(response.getBytes()));
}
}
}
Non-blocking I/O 의 메커니즘은?
답:
1. OS 의 비차단 모드:
단순 폴링의 문제:
멀티플렉싱 (select/poll/epoll):
자바 추상화:
결과:
Selector 의 구성:
Selector (등록된 채널 모니터)
│
├── SelectionKey 1 (Channel A + 관심 이벤트)
├── SelectionKey 2 (Channel B + 관심 이벤트)
└── SelectionKey 3 (Channel C + 관심 이벤트)
Channel 등록 시:
- Channel + interest ops → SelectionKey
- SelectionKey 가 Selector 에 추가
select() 호출:
- 등록된 모든 SelectionKey 의 채널 검사
- 준비된 이벤트 있으면 selectedKeys() 에 추가
- 없으면 대기 (또는 timeout)
// 4가지 이벤트 (interest ops)
SelectionKey.OP_READ // 읽기 준비됨
SelectionKey.OP_WRITE // 쓰기 준비됨 (대부분 항상 준비됨, 조심!)
SelectionKey.OP_ACCEPT // 연결 수락 가능 (ServerSocketChannel)
SelectionKey.OP_CONNECT // 연결 완료됨 (SocketChannel)
// 채널 종류별 지원 이벤트
// ServerSocketChannel: OP_ACCEPT
// SocketChannel: OP_READ, OP_WRITE, OP_CONNECT
// FileChannel: 지원 X! (NIO Selector 와 호환 안 됨, NIO.2 의 AsynchronousFileChannel 사용)
// 여러 이벤트 결합
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
// 준비된 이벤트 확인
SelectionKey key = ...;
key.isAcceptable(); // 연결 수락 가능?
key.isReadable(); // 데이터 읽기 가능?
key.isWritable(); // 쓰기 가능?
key.isConnectable(); // 연결 완료?
// readyOps() — 준비된 이벤트 비트맵
int ready = key.readyOps();
if ((ready & SelectionKey.OP_READ) != 0) { ... }
// interestOps() — 관심 이벤트
int interest = key.interestOps();
// 관심 이벤트 변경
key.interestOps(SelectionKey.OP_WRITE); // 쓰기만 관심
// 채널마다 상태 저장
ByteBuffer buffer = ByteBuffer.allocate(1024);
SelectionKey key = channel.register(selector, OP_READ, buffer);
// ↑ attachment
// 나중에 꺼내기
SelectionKey key = ...;
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 또는 상태 객체
class ClientState {
SocketChannel channel;
ByteBuffer readBuffer;
ByteBuffer writeBuffer;
String currentRequest;
}
ClientState state = new ClientState();
channel.register(selector, OP_READ, state);
// 사용
SelectionKey key = ...;
ClientState state = (ClientState) key.attachment();
// select() — 무한 대기
int n = selector.select();
// 이벤트 발생까지 대기
// n = 준비된 채널 수
// select(timeout) — 타임아웃
int n = selector.select(1000); // 1초
// 1초 안에 이벤트 없으면 0 반환
// selectNow() — 즉시 리턴
int n = selector.selectNow();
// 폴링 (즉시 확인)
// 이벤트 없으면 0
// wakeup() — 다른 스레드에서 select 깨움
selector.wakeup();
// select 가 즉시 리턴 (n=0 가능)
// 완전한 Echo 서버 예시
public class NioEchoServer {
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
// 서버 소켓 채널
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(PORT));
server.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port " + PORT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 처리 후 반드시 제거
if (!key.isValid()) continue;
try {
if (key.isAcceptable()) {
handleAccept(server, selector);
} else if (key.isReadable()) {
handleRead(key);
} else if (key.isWritable()) {
handleWrite(key);
}
} catch (IOException e) {
key.cancel();
key.channel().close();
}
}
}
}
private static void handleAccept(ServerSocketChannel server, Selector selector)
throws IOException {
SocketChannel client = server.accept();
client.configureBlocking(false);
// 각 클라이언트마다 버퍼
ByteBuffer buffer = ByteBuffer.allocate(1024);
client.register(selector, SelectionKey.OP_READ, buffer);
}
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
int n = client.read(buffer);
if (n == -1) {
client.close();
return;
}
// Echo — 받은 데이터를 그대로 쓰기로 전환
buffer.flip();
key.interestOps(SelectionKey.OP_WRITE);
}
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
client.write(buffer);
if (!buffer.hasRemaining()) {
buffer.clear();
key.interestOps(SelectionKey.OP_READ); // 다시 읽기로
}
}
}
주의사항:
1. selectedKeys() 반드시 제거
- it.remove() 안 하면 다음 select 에서 또 나옴
- 이벤트 중복 처리
2. OP_WRITE 의 함정
- 대부분 항상 준비됨 (소켓 버퍼에 여유)
- 무한 루프 가능
- 데이터 있을 때만 등록
3. Invalid Key
- 채널 close 후 key.isValid() = false
- 명시적 처리 필요
4. select() 의 spurious wakeup
- 이벤트 없어도 깨어날 수 있음
- selectedKeys 가 비어있을 수 있음
- n == 0 처리
5. 한 스레드만 안전
- Selector 와 SelectionKey 는 멀티스레드 안전 X
- 한 스레드에서만 사용
// Selector + 한 스레드 = EventLoop
public class EventLoop {
private final Selector selector;
private final Thread thread;
private volatile boolean running = true;
public EventLoop() throws IOException {
this.selector = Selector.open();
this.thread = new Thread(this::run);
}
public void start() {
thread.start();
}
private void run() {
while (running) {
try {
selector.select();
processEvents();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void processEvents() throws IOException {
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
// 처리
}
}
public void stop() {
running = false;
selector.wakeup();
}
}
// Netty 의 EventLoop 가 이 패턴
// 한 스레드 + Selector = 한 EventLoop
// 여러 EventLoop = EventLoopGroup
Selector 의 정밀 동작과 주의점은?
답:
1. 구조:
동작:
이벤트:
주의:
활용:
2가지 축의 조합:
축 1: Blocking vs Non-blocking
- 호출자가 정지하는가?
축 2: Sync vs Async
- 결과를 누가 알려주는가?
4가지 조합:
1. Sync + Blocking → 전통 IO
2. Sync + Non-blocking → Selector
3. Async + Blocking → 거의 없음
4. Async + Non-blocking → 콜백, Future
Sync (동기):
결과를 호출자가 직접 확인.
- read(): 결과를 직접 받음
- select(): 준비된 채널 직접 확인
- 호출자가 작업 추적
Sync + Blocking:
- read() 호출 → 결과 받을 때까지 정지
Sync + Non-blocking:
- read() 호출 → 즉시 리턴
- 호출자가 다시 시도 또는 폴링
Async (비동기):
결과를 시스템/콜백이 알려줌.
- 호출은 즉시 리턴
- 작업은 백그라운드에서 진행
- 완료 시 콜백/Future/Promise 로 알림
Async + Non-blocking:
- 콜백 등록
- 작업 시작 (즉시 리턴)
- 완료 시 자동 호출
// 1. Sync + Blocking (java.io)
FileInputStream fis = new FileInputStream("file.txt");
int b = fis.read(); // 직접 받음, 대기
// 2. Sync + Non-blocking (java.nio + Selector)
SocketChannel ch = ...;
ch.configureBlocking(false);
int n = ch.read(buffer); // 즉시 리턴, 직접 결과
if (n == 0) { /* 다시 시도 */ }
// 3. Async + Blocking
// 일반적으로 존재하지 않음 (모순적)
// 4. Async + Non-blocking (CompletableFuture, AsynchronousChannel)
AsynchronousFileChannel ch = AsynchronousFileChannel.open(path);
Future<Integer> future = ch.read(buffer, 0);
// 즉시 리턴, 결과는 future 로
// 또는 콜백
ch.read(buffer, 0, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
// 완료 시 호출
}
@Override
public void failed(Throwable exc, Object attachment) {
// 실패 시
}
});
// Java 8+ CompletableFuture
public CompletableFuture<Shipment> findShipmentAsync(Long id) {
return CompletableFuture.supplyAsync(() -> {
return repository.findById(id).orElseThrow();
});
}
// 체이닝
findShipmentAsync(id)
.thenCompose(s -> enrichWithTrackingAsync(s))
.thenApply(s -> ShipmentResponse.from(s))
.thenAccept(response -> sendToClient(response))
.exceptionally(t -> {
log.error("Failed", t);
return null;
});
// 결합
CompletableFuture<Shipment> ship = findShipmentAsync(id);
CompletableFuture<TrackingInfo> track = trackingApi.fetchAsync(id);
ship.thenCombine(track, (s, t) -> ShipmentResponse.from(s, t))
.thenAccept(response -> sendToClient(response));
// 콜백 지옥 (Callback Hell)
asyncApi.fetch(id, response1 -> {
asyncApi2.process(response1, response2 -> {
asyncApi3.save(response2, response3 -> {
asyncApi4.notify(response3, response4 -> {
// 4단계 중첩 — 가독성 ↓
});
});
});
});
// CompletableFuture 로 평탄화
asyncApi.fetch(id)
.thenCompose(r1 -> asyncApi2.process(r1))
.thenCompose(r2 -> asyncApi3.save(r2))
.thenCompose(r3 -> asyncApi4.notify(r3))
.thenAccept(r4 -> { /* 처리 */ });
// Reactive (Mono/Flux)
asyncApi.fetch(id)
.flatMap(asyncApi2::process)
.flatMap(asyncApi3::save)
.flatMap(asyncApi4::notify)
.subscribe();
| 모델 | 호출 | 결과 받기 | 자바 예 |
|---|---|---|---|
| Sync + Blocking | 대기 | 직접 | InputStream.read() |
| Sync + Non-blocking | 즉시 | 직접 | SocketChannel.read() + Selector |
| Async + Non-blocking | 즉시 | 콜백/Future | CompletableFuture |
| Async + Blocking | (드뭄) | 콜백 | — |
용어의 혼란:
"비동기" 라는 단어가 두 가지로 쓰임:
1. Non-blocking 의 의미
2. Async (콜백) 의 의미
엄밀히:
- Non-blocking: 함수가 즉시 리턴
- Async: 결과를 콜백/Future 로
자바 NIO 의 Selector:
- Non-blocking 하지만 Sync
- 호출자가 selectedKeys 로 직접 확인
자바 CompletableFuture:
- Async + Non-blocking
- 콜백/Future 로 결과
Sync/Async 와 Blocking/Non-blocking 의 정확한 차이는?
답:
1. Blocking vs Non-blocking:
Sync vs Async:
4가지 조합:
자바 NIO Selector:
CompletableFuture:
1. Thread-per-connection (전통)
- 1 연결 = 1 스레드
- Tomcat 기본
2. Event Loop (NIO + Selector)
- 1 스레드 = N 연결
- Netty, Node.js
3. Reactive (Publisher-Subscriber)
- Stream of events
- Spring WebFlux, Reactor
4. Virtual Threads (Project Loom, Java 21+)
- 가상 스레드 (저렴함)
- 1 가상 스레드 = 1 연결
- 가독성 + 효율
// 가장 단순
ServerSocket server = new ServerSocket(8080);
ExecutorService pool = Executors.newCachedThreadPool();
while (true) {
Socket client = server.accept();
pool.submit(() -> handleClient(client));
}
private void handleClient(Socket client) {
// Blocking 코드
String request = readRequest(client);
String response = process(request);
writeResponse(client, response);
}
// 장점:
// - 코드 단순
// - 디버깅 쉬움
// - 가독성 ↑
// 단점:
// - 스레드 폭증 (1만 연결 = 1만 스레드)
// - 메모리 ↑
// - 컨텍스트 스위칭
// Netty 의 EventLoop
public class EchoServer {
public void start() throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new EchoHandler());
}
});
b.bind(8080).sync().channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
class EchoHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ctx.writeAndFlush(msg);
// 이 메서드는 EventLoop 스레드에서 실행
// Blocking 작업 금지! (다른 연결 막힘)
}
}
// 장점:
// - 적은 스레드 (CPU 코어 수)
// - 큰 동시성
// - 메모리 효율
// 단점:
// - Blocking 작업 금지 (까다로움)
// - 코드 복잡
// - 디버깅 어려움
@RestController
public class ReactiveShipmentController {
private final ShipmentRepository repository;
@GetMapping("/api/shipments/{id}")
public Mono<ShipmentResponse> get(@PathVariable Long id) {
return repository.findById(id)
.flatMap(this::enrichWithTracking)
.map(ShipmentResponse::from);
// 모든 작업이 Reactive
// Non-blocking
}
@GetMapping("/api/shipments")
public Flux<ShipmentResponse> list() {
return repository.findAll()
.map(ShipmentResponse::from);
// Stream of items
}
private Mono<EnrichedShipment> enrichWithTracking(Shipment s) {
return trackingApi.fetchAsync(s.getTrackingNumber())
.map(tracking -> new EnrichedShipment(s, tracking));
}
}
// 장점:
// - 함수형 스타일
// - 자연스러운 비동기
// - 백프레셔 (Backpressure) 지원
// - 메모리 효율
// 단점:
// - 학습 곡선 가파름
// - 디버깅 어려움
// - 스택 트레이스 복잡
// Java 21+ 의 가상 스레드
public class VirtualThreadServer {
public void start() throws IOException {
ServerSocket server = new ServerSocket(8080);
// 가상 스레드 풀
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
while (true) {
Socket client = server.accept();
executor.submit(() -> handleClient(client));
// 매 요청마다 새 가상 스레드
// 가상 스레드는 거의 무료!
}
}
private void handleClient(Socket client) {
// 동기 코드 그대로
// Blocking I/O 도 OK
String request = readRequest(client); // Blocking
String response = process(request);
writeResponse(client, response);
// 가상 스레드가 Blocking 되어도
// OS 스레드는 다른 가상 스레드 실행
}
}
// 장점:
// - 코드는 동기 (가독성 ↑)
// - 가상 스레드 비용 매우 낮음 (수 KB)
// - 1만 연결 = 1만 가상 스레드 가능
// - 디버깅 쉬움 (스택 트레이스 명확)
// 원리:
// - 가상 스레드 = 사용자 공간 스케줄러
// - OS 스레드 (carrier thread) 위에서 동작
// - Blocking I/O 만나면 가상 스레드 unmount
// - OS 스레드는 다른 가상 스레드 실행
| 모델 | 스레드 | 가독성 | 성능 | 도입 |
|---|---|---|---|---|
| Thread-per-connection | 연결 수 | ↑↑ | ↓ (대규모) | Java 1.0+ |
| Event Loop (NIO/Netty) | 코어 수 | ↓ | ↑↑ | Java 1.4+ |
| Reactive | 코어 수 | ↓ | ↑↑ | Java 8+ (라이브러리) |
| Virtual Threads | 가상 스레드 무한 | ↑↑ | ↑↑ | Java 21+ |
1만 동시 연결 처리:
Thread-per-connection (Tomcat 기본):
OS 스레드: 1만 개
메모리: 10GB
코드: 동기, 직관적
Event Loop (Netty):
OS 스레드: 8개 (CPU 코어)
메모리: 수십 MB
코드: 콜백, 복잡
Reactive (WebFlux):
OS 스레드: 8개
메모리: 수십 MB
코드: Mono/Flux, 함수형
Virtual Threads:
OS 스레드: 8개
가상 스레드: 1만 개
메모리: 수십~수백 MB
코드: 동기, 직관적
ILIC 시나리오별 권장:
1. 일반 웹 서비스 (Tomcat):
- Thread-per-connection
- 단순, 충분
- Spring MVC
2. 대규모 동시 처리 필요:
- Spring WebFlux (Reactive)
- 또는 Project Loom (Java 21+)
3. 마이크로서비스 게이트웨이:
- Netty 기반
- Spring Cloud Gateway
4. 미래 (Java 21+):
- Project Loom 의 Virtual Threads
- 코드 단순 + 효율
4가지 동시성 모델의 비교는?
답:
1. Thread-per-connection:
Event Loop (Netty):
Reactive (WebFlux):
Virtual Threads (Loom):
선택:
C10K 문제:
"Concurrent 10,000 connections"
- 한 서버에서 1만 동시 연결 처리
- 1999 년 Dan Kegel 이 제기
- 인터넷 폭발 시대의 도전
해결:
- Non-blocking I/O
- select/poll/epoll
- Event-driven 아키텍처
현재:
- C10K → C10M (1천만)
- C100M 으로 확장
가정:
- 각 연결의 I/O 대기 시간 90%
- 활성 CPU 사용 10%
- 평균 응답 시간 100ms
Blocking + 1만 스레드:
자원:
- 메모리: 1MB × 10,000 = 10GB
- OS 한도 (ulimit, /proc/sys/kernel/threads-max)
- 컨텍스트 스위칭: 초당 수만 번
실제 처리:
- 처리량: 1만 req / 100ms = 10만 req/sec
- 이론상
문제:
- 메모리 한계
- 컨텍스트 스위칭 폭발
- 실제로는 처리량 ↓ (스위칭 오버헤드)
Non-blocking + Selector + 4 스레드:
자원:
- 메모리: 약 100MB (Buffer 등)
- OS 스레드: 4
- epoll 이벤트 관리
실제 처리:
- 4 스레드가 1만 연결 모니터
- CPU 캐시 효율 ↑
- 컨텍스트 스위칭 최소
처리량:
- 일반적으로 Blocking 보다 높음
- 최대 처리량의 90~95%
장점:
- 메모리 1/100
- CPU 효율
- 안정성
Virtual Threads + 8 OS 스레드:
자원:
- OS 스레드: 8 (CPU 코어)
- 가상 스레드: 1만 (각 1KB ~ 8KB)
- 메모리: 약 50MB
처리:
- 가상 스레드 = 1 연결
- Blocking I/O 시 unmount → 다른 가상 스레드
- 동기 코드 그대로
처리량:
- Non-blocking 과 비슷
- 코드는 단순
장점:
- 가독성 ↑
- 디버깅 쉬움
- 효율 ↑
시나리오: HTTP 요청 1만 개 동시
처리 내용: DB 조회 50ms + CPU 5ms
Tomcat + Blocking + 200 스레드 풀:
- 동시 처리: 200
- 나머지 9,800 대기
- 응답 시간: 1차 100ms, 2차 200ms, ...
- 총 처리: 약 25초
Netty + Non-blocking + 8 스레드:
- 동시 처리: 1만
- 응답 시간: 55ms (모두 비슷)
- 총 처리: 약 0.1초
Spring WebFlux + Reactive:
- 동시 처리: 1만
- 응답 시간: 55ms
- 총 처리: 약 0.1초
Tomcat + Virtual Threads (Java 21+):
- 동시 처리: 1만 (가상 스레드)
- 응답 시간: 55ms
- 총 처리: 약 0.1초
- 코드: 동기, 단순
시나리오별 권장:
1. 모니터링/분석 시스템 (자주 호출):
- Reactive 또는 Loom
- 응답 시간 일관성
2. 일반 비즈니스 API:
- Tomcat + Blocking
- 사용자 수가 적당 (< 1천)
3. 대규모 이벤트 처리:
- Netty 또는 Reactive
- 초당 1만+ 요청
4. 미래 시스템:
- Java 21+ Virtual Threads
- 코드 단순 + 효율
ILIC 의 실제 시나리오:
평소 (100 동시):
- Tomcat + Blocking 충분
- 200 스레드 풀로 OK
피크 (1,000 동시):
- Tomcat 한계 시작
- Connection Pool 증대
- Cache 활용
- 또는 Reactive 전환
블랙 프라이데이 (10,000 동시):
- Reactive 또는 Loom 필요
- 또는 수평 확장
- CDN, Load Balancer
향후 (Java 21+):
- Virtual Threads 채택
- 기존 코드 거의 그대로
- 효율 대폭 향상
1만 동시 연결의 처리는?
답:
1. Blocking:
Non-blocking + Selector:
Reactive:
Virtual Threads (Java 21+):
Tomcat 의 모델:
Bio (Blocking):
- 옛 Tomcat (5.5 이전)
- Thread-per-connection
- 거의 사용 X
Nio (Non-blocking, 기본 Tomcat 8+):
- Selector 활용
- 한 스레드가 다수 연결의 시작 처리
- 비즈니스 로직은 워커 스레드 풀에서 (Blocking)
설정:
server.tomcat.threads.max=200 # 최대 워커 스레드
server.tomcat.threads.min-spare=10 # 최소 유휴
server.tomcat.max-connections=10000 # 동시 연결 (대기 포함)
server.tomcat.accept-count=100 # 큐 크기
특징:
- 하이브리드: NIO Acceptor + Blocking Worker
- 코드는 Blocking 처럼
- I/O 시작/완료는 Non-blocking
Netty:
- 순수 Non-blocking
- EventLoopGroup + EventLoop
- boss group (accept) + worker group (read/write)
EventLoopGroup:
- 보통 CPU 코어 수만큼
- bossGroup: 1 (accept 만)
- workerGroup: N (실제 I/O)
EventLoop:
- 한 스레드 + 하나의 Selector
- 채널들을 round-robin 또는 hash 로 분배
활용:
- gRPC, Spring WebFlux, Spring Cloud Gateway
- 대규모 동시성 서비스
- 자체 프로토콜
Spring WebFlux:
- Reactive 웹 프레임워크
- Netty 기반 (또는 Tomcat/Jetty/Undertow)
- Spring 5+
핵심 타입:
- Mono<T>: 0~1 개
- Flux<T>: 0~N 개
특징:
- Non-blocking
- 백프레셔 (Backpressure)
- 함수형
- Project Reactor
예시:
@GetMapping
public Flux<Item> list() {
return repository.findAll();
}
// Mono.zip 으로 병렬 결합
Mono.zip(getUser(id), getOrders(id))
.map(t -> new UserOrders(t.getT1(), t.getT2()));
Project Loom 의 핵심:
Virtual Threads:
- 가상 스레드 (사용자 공간)
- 매우 가벼움 (수 KB)
- 무제한 생성 가능
Carrier Threads:
- 실제 OS 스레드
- 가상 스레드의 실행자
- 기본 ForkJoinPool
동작:
1. 가상 스레드 시작 → carrier thread 에 mount
2. CPU 작업 실행
3. Blocking I/O 도달 → JVM 이 감지
4. 가상 스레드 unmount → carrier thread 자유
5. carrier thread 가 다른 가상 스레드 실행
6. I/O 완료 시 가상 스레드 mount → 계속
장점:
- 동기 코드 (가독성 ↑)
- Non-blocking 효율
- 1억 가상 스레드도 가능 (메모리 한도 내)
// Java 21+
public class LoomServer {
public void start() throws IOException {
ServerSocket server = new ServerSocket(8080);
// 가상 스레드 Executor
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
while (true) {
Socket client = server.accept();
executor.submit(() -> handleClient(client));
// 매 연결당 새 가상 스레드 — 비용 거의 없음
}
}
private void handleClient(Socket client) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter writer = new PrintWriter(client.getOutputStream())) {
// 동기 코드 — Blocking 처럼 보이지만
// 실제로는 가상 스레드가 unmount 됨
String request = reader.readLine();
String response = process(request);
writer.println(response);
} catch (IOException e) {
// 처리
}
}
}
# Java 21 + Spring Boot 3.2+
spring:
threads:
virtual:
enabled: true # ★ Virtual Threads 활성화
# 효과:
# - Tomcat 의 워커 스레드가 가상 스레드로
# - @Async 도 가상 스레드로
# - 코드 변경 없이 효율 ↑
2024+ 권장:
1. Java 21+ 사용 가능?
- YES → Virtual Threads 활성화
- 코드 단순 + 효율
2. Java 17 사용?
- Spring WebFlux 고려
- 또는 일반 Tomcat (작은 규모)
3. 대규모 동시성 필수?
- Reactive 또는 Virtual Threads
- 절대 Blocking + 적은 스레드 풀 X
4. 레거시?
- Tomcat + 적절한 스레드 풀
- Connection Pool, Cache 활용
Non-blocking 의 단점:
1. 코드 복잡
- 콜백, Mono/Flux, Selector
- 학습 곡선
2. 디버깅 어려움
- 스택 트레이스 복잡
- 흐름 추적 어려움
3. CPU bound 작업에 부적합
- 이미지 처리, 암호화
- Blocking + 멀티스레드가 더 효율
4. 라이브러리 의존
- 모든 라이브러리가 Non-blocking 지원해야
- DB 드라이버, Redis 등
5. 작은 시스템에 과함
- 100 동시 연결이면 Blocking OK
- 코드 단순함이 더 중요
결론:
- 대규모: Reactive 또는 Loom
- 일반: Blocking (Tomcat) + 적절한 스레드 풀
- 미래: Loom (Virtual Threads)
// 권장 패턴 (Java 17, Spring Boot 3.x)
@RestController
public class ShipmentController {
@GetMapping("/api/shipments/{id}")
public ShipmentResponse get(@PathVariable Long id) {
// 동기 코드 — Tomcat NIO + 워커 스레드
return service.findById(id);
}
}
// 향후 (Java 21+)
spring:
threads:
virtual:
enabled: true
// 코드 그대로, Virtual Threads 활성화
// 효율 대폭 향상
Tomcat/Netty/WebFlux/Loom 의 동시성 모델은?
답:
1. Tomcat (NIO):
Netty:
Spring WebFlux:
Project Loom (Java 21+):
Q1. I/O 의 정의?
A1. JVM 기준 외부와의 데이터 흐름
Q2. 콘솔 출력은 Input/Output?
A2. Output (JVM 기준)
Q3. JVM 기준이 중요한 이유?
A3. 일관된 명명, 헷갈림 해소
Q4. I/O 의 4가지 종류?
A4. 콘솔, 파일, 네트워크, 메모리
Q5. CPU 와 I/O 의 속도 차이?
A5. 수천~수억배 (네트워크 가장 느림)
Q6. I/O bound 의 의미?
A6. I/O 대기가 병목
Q7. 자바 I/O 3 시대?
A7. java.io (1.0), java.nio (1.4), nio.file (7)
Q8. 자바 I/O 패키지 4가지?
A8. java.io, java.nio, java.nio.channels, java.nio.file
Q9. 표준 입력의 타입?
A9. System.in = InputStream
Q10. System.out 의 타입?
A10. PrintStream (Output)
Q11. IO 의 5가지 한계?
A11. 1바이트, 단방향, Blocking, Decorator, File
Q12. NIO 의 3가지 핵심?
A12. Channel, Buffer, Selector
Q13. Channel 의 정의?
A13. 양방향 데이터 통로
Q14. Buffer 의 4속성?
A14. capacity, position, limit, mark
Q15. 불변식?
A15. 0 ≤ mark ≤ position ≤ limit ≤ capacity
Q16. flip 의 동작?
A16. limit = position, position = 0
Q17. clear 의 동작?
A17. position = 0, limit = capacity
Q18. Heap vs Direct Buffer?
A18. heap 안 vs 밖, 복사 횟수
Q19. zero-copy?
A19. CPU 복사 회피 (transferTo)
Q20. File vs Files?
A20. 인스턴스 vs static, 명확한 예외
Q21. NIO.2 의 등장 시기?
A21. Java 7 (2011)
Q22. NIO.2 의 핵심?
A22. Path, Files, WatchService, Async
Q23. Path 의 특징?
A23. 불변, java.nio.file
Q24. Files 의 주요 메서드?
A24. exists, readString, lines, walk, copy
Q25. resolve vs normalize?
A25. 결합 vs 정규화 (./.. 제거)
Q26. relativize?
A26. 한 경로에서 다른 경로의 상대
Q27. Files.lines?
A27. Stream<String> 반환, 한 줄씩
Q28. WatchService?
A28. 파일 변경 감지
Q29. AsynchronousFileChannel?
A29. 비동기 파일 I/O
Q30. FileSystem 추상화?
A30. ZIP, 메모리 등 다양한 FS
Q31. Blocking 의 정의?
A31. 호출 시 스레드 정지
Q32. Non-blocking 의 정의?
A32. 즉시 리턴
Q33. Blocking 의 OS 동작?
A33. 스레드를 wait queue 에 등록
Q34. Interrupt 가 Blocking 못 깨우는 이유?
A34. OS 의 D 상태 (uninterruptible)
Q35. Blocking 빠져나오는 방법?
A35. 스트림 close
Q36. Non-blocking 의 폴링 문제?
A36. CPU 100%
Q37. select/poll/epoll?
A37. OS 의 멀티플렉싱 시스템 호출
Q38. epoll 의 장점?
A38. O(1), 등록한 fd 만
Q39. Selector 의 본질?
A39. epoll/kqueue/IOCP 의 자바 추상화
Q40. SelectionKey 의 이벤트?
A40. OP_READ, OP_WRITE, OP_ACCEPT, OP_CONNECT
Q41. Sync vs Async?
A41. 결과를 직접 vs 콜백
Q42. C10K 문제?
A42. 1만 동시 연결 처리
Q43. 1만 연결 Blocking 메모리?
A43. 10GB (1MB × 10,000)
Q44. 1만 연결 Non-blocking 메모리?
A44. 약 100MB
Q45. Tomcat 의 동시성?
A45. NIO + 워커 스레드 풀 (하이브리드)
Q46. Netty 의 모델?
A46. EventLoop (순수 Non-blocking)
Q47. Spring WebFlux?
A47. Reactive (Mono/Flux), Netty 기반
Q48. Project Loom 의 가상 스레드?
A48. 무료 가상 스레드, 동기 코드 + 효율
Q49. Virtual Threads 활성화?
A49. spring.threads.virtual.enabled=true
Q50. Phase 7 마스터 후?
A50. 자바 I/O 자유자재 + 동시성 모델 선택
50 / 50 → Phase 7 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 7.1 ~ 7.4 재학습
Phase 7 — I/O 시스템 큰 그림
Unit 7.1 — I/O 란 무엇인가
- JVM 기준 Input/Output
- 종류, 진화, 성능, 모델
Unit 7.2 — IO vs NIO (역사적 진화)
- Java 1.0 → 1.4 → 7
- File → FileChannel → Files
- Stream → Channel + Buffer
Unit 7.3 — Stream vs Channel
- Buffer 의 4속성 정밀
- Heap vs Direct
- zero-copy
Unit 7.4 — Blocking vs Non-blocking (마스터)
- OS 레벨 동작
- Selector 정밀
- 동시성 모델 4가지
- 1만 연결 시나리오
- Tomcat/Netty/WebFlux/Loom
1. I/O 모델 선택
- 시나리오에 맞는 모델
- 자원 사용 분석
2. Tomcat 설정
- 스레드 풀
- Connection Pool
- 최적화
3. Spring WebFlux 활용
- Reactive 프로그래밍
- Mono/Flux
4. 면접 자신감
- 모든 I/O 질문
- 동시성 모델
- C10K 문제
5. Java 21+ 활용
- Virtual Threads
- 동기 코드 + 효율
- 미래 표준
✅ Phase 1 — Pass by Value (3 Unit)
✅ Phase 2 — 컬렉션 프레임워크 (6 Unit)
✅ Phase 3 — 해시의 원리 (4 Unit)
✅ Phase 4 — 추상화의 두 도구 (4 Unit)
✅ Phase 5 — 제네릭과 와일드카드 (5 Unit)
✅ Phase 6 — 객체 비교 (4 Unit)
🚀 Phase 7 — I/O 시스템 큰 그림 (4/5 진행, 다음 마지막)
⏭ Phase 8 — Stream 실전
⏭ Phase 9 — I/O 강화
⏭ Phase 10 — 함수형 프로그래밍
총: 30/43 Unit 작성 (약 70%)
1. Blocking vs Non-blocking
2. Selector 의 본질
3. 동시성 모델 4가지
이번 Unit에서 동시성 모델을 봤다면, 다음은 오버헤드와 File 객체 — 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 객체 (Phase 7 완주)