3주차 Unit 7.4 — Blocking vs Non-blocking

Psj·2026년 5월 19일

F-lab

목록 보기
104/197

Unit 7.4 — Blocking vs Non-blocking

F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
🎯 마스터 프롬프트 깊이 Unit


📌 학습 목표

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

  • Blocking I/O 의 정확한 동작 (OS 레벨까지) 은?
  • Blocking 의 5가지 구조적 문제 는?
  • Blocking 에서 빠져나오는 방법 은 왜 close() 뿐인가? (interrupt 가 왜 안 통하는가?)
  • Non-blocking I/O 의 메커니즘 (select/poll/epoll) 은?
  • Selector 의 멀티플렉싱 정밀 동작은?
  • Sync vs Async 의 정확한 차이 (Blocking/Non-blocking 과 어떻게 다른가)?
  • C10K 문제 와 동시성 모델의 진화는?
  • 1만 동시 연결 을 각 모델로 처리 시 자원 사용은?
  • Tomcat NIO, Netty, Spring WebFlux, Project Loom 의 동시성 모델은?
  • Non-blocking 이 항상 좋은가? (CPU bound 에서)
  • Phase 7 의 50가지 졸업 시험 통과 가능?

🎯 핵심 한 문장

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 모델 = 동시성과 자원의 트레이드오프.


🧭 9개 섹션 로드맵

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 졸업 시험 + 완주

1️⃣ Blocking I/O 의 정확한 동작

1.1 Blocking 의 정의

Blocking I/O:

  I/O 함수 호출 → 작업 완료까지 호출자 (스레드) 정지.
  
  완료 = "사용 가능한 데이터 있음" 또는 "쓰기 가능"
  
정지 동안:
  - 스레드는 다른 일 못 함
  - 다른 데이터 처리 X
  - 인터럽트도 거의 안 통함

1.2 read() 의 OS 레벨 동작

사용자 코드:
  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 가 스케줄러에서 제외
  - 깨우는 조건 = 데이터 도착

1.3 스레드 상태 (Linux)

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() 만

1.4 Blocking read 의 시뮬레이션

// 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);
    }
}

1.5 Interrupt 의 한계

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.6 Blocking 에서 빠져나오는 방법

// 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 로 인한 종료
}

1.7 Blocking 의 자원 사용

Blocking 모델의 자원:

연결 1 = 스레드 1

스레드의 자원:
  - 스택 메모리 (기본 1MB, JVM 옵션 -Xss)
  - 커널 자원 (스케줄러 관리)
  - 컨텍스트 스위칭 비용 (~1μs)
  - TLB cache miss 비용

100 연결:
  - 메모리: 100MB
  - OK

1,000 연결:
  - 메모리: 1GB
  - 한계 시작

10,000 연결:
  - 메모리: 10GB
  - 컨텍스트 스위칭 폭발
  - 비현실적

1.8 Blocking 의 단순함 (장점)

// 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) {
            // 처리
        }
    }
}

// 장점:
// - 직관적 흐름
// - 디버깅 쉬움
// - 가독성 ↑
// - 작은 서버에 충분

1.9 ILIC 의 Blocking 활용

// 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
// - 그 이상은 큐에 대기

1.10 자기 점검 답변

Blocking I/O 에서 스레드는 어떤 상태가 되나?

:
1. OS 레벨:

  • Linux: TASK_INTERRUPTIBLE (S) 또는 TASK_UNINTERRUPTIBLE (D)
  • wait queue 에 등록
  • 스케줄러에서 제외
  1. 자바 레벨:

    • Thread.State.WAITING / BLOCKED
    • 데이터 도착까지 대기
  2. 빠져나오는 방법:

    • 스트림 close() (가장 일반)
    • SO_TIMEOUT (소켓)
    • NIO 의 InterruptibleChannel 은 interrupt 가능
  3. interrupt 가 안 통하는 이유:

    • 일부 I/O 는 D 상태 (uninterruptible)
    • 일반 자바 sleep/wait/join 과 다름

2️⃣ Blocking 의 5가지 구조적 문제

2.1 문제 1 — 스레드 폭증

연결 = 스레드 1:1 매핑

문제:
  100 연결 → 100 스레드 (OK)
  1,000 연결 → 1,000 스레드 (한계)
  10,000 연결 → 10,000 스레드 (불가능)

원인:
  스레드 비용:
  - 메모리 (1MB 스택)
  - 커널 자원 (PCB)
  - 컨텍스트 스위칭

해결:
  - 스레드 풀로 일부 완화
  - Non-blocking 으로 근본 해결

2.2 문제 2 — 컨텍스트 스위칭 비용

컨텍스트 스위칭:
  - CPU 가 스레드 A 에서 B 로 전환
  - 레지스터 저장/복원
  - TLB (Translation Lookaside Buffer) 무효화
  - CPU 캐시 invalidation
  
비용:
  - 1~10 마이크로초 (μs)
  - 작아 보이지만 자주 일어나면 누적
  
1만 스레드 시:
  - 초당 수만 번 스위칭
  - CPU 시간의 큰 부분이 스위칭에
  - 실제 작업 시간 ↓

2.3 문제 3 — 메모리 사용

스레드의 메모리:
  - 스택 (기본 1MB)
  - TLS (Thread Local Storage)
  - 커널 데이터

1만 스레드:
  - 스택만 10GB
  - + 커널 데이터 = 더
  - 64GB 서버도 부담
  - 16GB 서버는 불가

2.4 문제 4 — Interrupt 의 한계

// 일반 코드의 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) { ... }
    }
}

이유:

  • 일반 코드: JVM 이 인터럽트 처리
  • Blocking I/O: OS 의 system call 안에 있음, JVM 이 개입 못 함

2.5 문제 5 — 디버깅과 모니터링

Blocking 의 디버깅 어려움:

1. 스레드 덤프
   - 1만 스레드의 스택 트레이스
   - 분석 매우 어려움

2. 누가 막혔나?
   - 어떤 연결이 응답 없음?
   - 추적 복잡

3. 메모리 모니터링
   - 1만 스레드의 메모리 사용
   - GC 영향

4. 재시작
   - 모든 스레드 정리 필요
   - 시간 ↑

2.6 스레드 풀의 한계

// 스레드 풀로 일부 완화
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개가 대기

2.7 비교 시각화

시나리오: 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
  - 같은 처리량
  - 메모리 효율 압도적

2.8 ILIC 의 Blocking 한계 분석

ILIC 시나리오:

평소: 100 동시 요청
  - Blocking + Tomcat 200 스레드: OK

피크: 1,000 동시 요청
  - Tomcat 200 스레드: 800 대기
  - 응답 시간 ↑
  - 일부 타임아웃

블랙 프라이데이: 10,000 동시 요청
  - 시스템 마비
  - Non-blocking 또는 수평 확장 필요

2.9 자기 점검 답변

Blocking 의 5가지 구조적 문제는?

:
1. 스레드 폭증: 1만 연결 = 1만 스레드
2. 컨텍스트 스위칭 비용: μs 단위, 누적 ↑
3. 메모리 사용: 1MB × 1만 = 10GB
4. Interrupt 한계: 일부 I/O 는 interrupt 안 통함
5. 디버깅 어려움: 1만 스레드 분석

해결:

  • Non-blocking + Selector
  • 또는 Project Loom (가상 스레드)

3️⃣ Non-blocking I/O 의 메커니즘

3.1 Non-blocking 의 정의

Non-blocking I/O:

  I/O 함수 호출 → 즉시 리턴.
  
  - 데이터 있으면 가져옴
  - 데이터 없으면 0 반환 (또는 즉시 알림)
  - 스레드 정지 X

원리:
  OS 의 시스템 호출이 이미 비차단 모드 지원
  - read(fd, ..., O_NONBLOCK)
  - 데이터 없으면 EAGAIN 에러
  - 자바는 0 반환으로 추상화

3.2 NIO 의 Non-blocking 설정

// 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);   // 즉시 리턴
// 일부만 쓸 수도 있음 (커널 버퍼 가득)

3.3 폴링의 문제

// 단순 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 호출)
// - 비효율
// - 좋지 않은 패턴

3.4 OS 의 select/poll/epoll

멀티플렉싱 시스템 호출의 진화:

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

3.5 epoll 의 동작

epoll 의 3가지 시스템 호출:

1. epoll_create:
   - epoll 인스턴스 생성
   - 커널의 데이터 구조 (Red-Black Tree)

2. epoll_ctl:
   - fd 추가/수정/삭제
   - 모니터링 이벤트 등록 (read, write 등)

3. epoll_wait:
   - 이벤트 발생까지 대기
   - 또는 timeout
   - 발생한 이벤트만 반환

장점:
  - 한 번 등록, 여러 번 wait
  - 발생한 이벤트만 (O(1))
  - 효율적

3.6 자바 NIO 의 Selector

// 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);
            }
        }
    }
}

3.7 Non-blocking vs Blocking 의 시스템 호출

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 모니터

3.8 ILIC 의 Non-blocking 활용 (이론)

// 실무에선 직접 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()));
        }
    }
}

3.9 자기 점검 답변

Non-blocking I/O 의 메커니즘은?

:
1. OS 의 비차단 모드:

  • O_NONBLOCK 플래그
  • read 가 즉시 리턴
  1. 단순 폴링의 문제:

    • CPU 100%
    • 비효율
  2. 멀티플렉싱 (select/poll/epoll):

    • 다수 fd 한꺼번에 모니터
    • 이벤트 있을 때만 깨어남
  3. 자바 추상화:

    • Selector = epoll/kqueue/IOCP
    • configureBlocking(false)
    • SelectionKey
  4. 결과:

    • 한 스레드로 다수 처리
    • CPU 효율

4️⃣ Selector — 멀티플렉서의 정밀

4.1 Selector 의 구조

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.2 SelectionKey 의 이벤트

// 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);

4.3 SelectionKey 의 상태

// 준비된 이벤트 확인
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);   // 쓰기만 관심

4.4 attachment 활용

// 채널마다 상태 저장
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();

4.5 select() 의 종류

// 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 가능)

4.6 Selector 사용의 전체 흐름

// 완전한 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);   // 다시 읽기로
        }
    }
}

4.7 Selector 의 함정

주의사항:

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
   - 한 스레드에서만 사용

4.8 EventLoop 패턴

// 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

4.9 자기 점검 답변

Selector 의 정밀 동작과 주의점은?

:
1. 구조:

  • Selector + SelectionKey + Channel
  • epoll/kqueue/IOCP 의 자바 추상화
  1. 동작:

    • register: 채널 + 관심 이벤트
    • select: 이벤트 대기
    • selectedKeys: 준비된 이벤트
  2. 이벤트:

    • OP_READ, OP_WRITE, OP_ACCEPT, OP_CONNECT
    • 비트 OR 로 결합
  3. 주의:

    • selectedKeys() 의 it.remove() 필수
    • OP_WRITE 무한 루프 주의
    • 한 스레드만 사용
  4. 활용:

    • EventLoop 패턴
    • Netty 의 기본
    • 한 스레드 + 수많은 채널

5️⃣ Sync vs Async 의 정확한 차이

5.1 4가지 I/O 모델 재정리

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

5.2 Sync 의 정의

Sync (동기):

  결과를 호출자가 직접 확인.
  
  - read(): 결과를 직접 받음
  - select(): 준비된 채널 직접 확인
  - 호출자가 작업 추적

Sync + Blocking:
  - read() 호출 → 결과 받을 때까지 정지

Sync + Non-blocking:
  - read() 호출 → 즉시 리턴
  - 호출자가 다시 시도 또는 폴링

5.3 Async 의 정의

Async (비동기):

  결과를 시스템/콜백이 알려줌.
  
  - 호출은 즉시 리턴
  - 작업은 백그라운드에서 진행
  - 완료 시 콜백/Future/Promise 로 알림

Async + Non-blocking:
  - 콜백 등록
  - 작업 시작 (즉시 리턴)
  - 완료 시 자동 호출

5.4 자바 표준의 4가지 모델

// 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) {
        // 실패 시
    }
});

5.5 CompletableFuture 의 활용

// 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));

5.6 콜백 지옥과 Reactive

// 콜백 지옥 (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();

5.7 Sync/Async + Blocking/Non-blocking 표

모델호출결과 받기자바 예
Sync + Blocking대기직접InputStream.read()
Sync + Non-blocking즉시직접SocketChannel.read() + Selector
Async + Non-blocking즉시콜백/FutureCompletableFuture
Async + Blocking(드뭄)콜백

5.8 실무에서의 의미

용어의 혼란:

"비동기" 라는 단어가 두 가지로 쓰임:
  1. Non-blocking 의 의미
  2. Async (콜백) 의 의미

엄밀히:
  - Non-blocking: 함수가 즉시 리턴
  - Async: 결과를 콜백/Future 로

자바 NIO 의 Selector:
  - Non-blocking 하지만 Sync
  - 호출자가 selectedKeys 로 직접 확인

자바 CompletableFuture:
  - Async + Non-blocking
  - 콜백/Future 로 결과

5.9 자기 점검 답변

Sync/Async 와 Blocking/Non-blocking 의 정확한 차이는?

:
1. Blocking vs Non-blocking:

  • "호출 시 정지하는가"
  • read() 가 결과를 기다리는가
  1. Sync vs Async:

    • "결과를 누가 알려주는가"
    • 호출자가 직접 vs 콜백/Future
  2. 4가지 조합:

    • Sync + Blocking: 전통 IO
    • Sync + Non-blocking: Selector
    • Async + Non-blocking: CompletableFuture
    • Async + Blocking: 거의 없음
  3. 자바 NIO Selector:

    • Non-blocking 하지만 Sync
    • 호출자가 selectedKeys 직접 확인
  4. CompletableFuture:

    • Async + Non-blocking
    • 진정한 비동기

6️⃣ 동시성 모델 비교 (스레드/이벤트/Reactive/Loom)

6.1 4가지 동시성 모델

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 연결
   - 가독성 + 효율

6.2 Thread-per-connection

// 가장 단순
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만 스레드)
// - 메모리 ↑
// - 컨텍스트 스위칭

6.3 Event Loop (Netty 스타일)

// 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 작업 금지 (까다로움)
// - 코드 복잡
// - 디버깅 어려움

6.4 Reactive (Spring WebFlux)

@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) 지원
// - 메모리 효율

// 단점:
// - 학습 곡선 가파름
// - 디버깅 어려움
// - 스택 트레이스 복잡

6.5 Virtual Threads (Project Loom, Java 21+)

// 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 스레드는 다른 가상 스레드 실행

6.6 4가지 모델 비교

모델스레드가독성성능도입
Thread-per-connection연결 수↑↑↓ (대규모)Java 1.0+
Event Loop (NIO/Netty)코어 수↑↑Java 1.4+
Reactive코어 수↑↑Java 8+ (라이브러리)
Virtual Threads가상 스레드 무한↑↑↑↑Java 21+

6.7 시각화

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
  코드: 동기, 직관적

6.8 ILIC 의 모델 선택

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
   - 코드 단순 + 효율

6.9 자기 점검 답변

4가지 동시성 모델의 비교는?

:
1. Thread-per-connection:

  • 1 연결 = 1 OS 스레드
  • 단순, 작은 규모
  1. Event Loop (Netty):

    • 1 스레드 = N 연결
    • 대규모, 복잡
  2. Reactive (WebFlux):

    • Stream of events
    • 함수형, 백프레셔
  3. Virtual Threads (Loom):

    • 가상 스레드 무료
    • 동기 코드 + 효율
    • 미래 표준

선택:

  • 작은 시스템: Thread-per-connection
  • 대규모: Reactive 또는 Loom
  • 코드 단순 + 효율: Loom (Java 21+)

7️⃣ 1만 동시 연결 시나리오

7.1 C10K 문제

C10K 문제:

  "Concurrent 10,000 connections"
  - 한 서버에서 1만 동시 연결 처리
  - 1999 년 Dan Kegel 이 제기
  - 인터넷 폭발 시대의 도전

해결:
  - Non-blocking I/O
  - select/poll/epoll
  - Event-driven 아키텍처

현재:
  - C10K → C10M (1천만)
  - C100M 으로 확장

7.2 1만 연결 — Blocking 모델

가정:
  - 각 연결의 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
  - 이론상
  
문제:
  - 메모리 한계
  - 컨텍스트 스위칭 폭발
  - 실제로는 처리량 ↓ (스위칭 오버헤드)

7.3 1만 연결 — Non-blocking 모델

Non-blocking + Selector + 4 스레드:

자원:
  - 메모리: 약 100MB (Buffer 등)
  - OS 스레드: 4
  - epoll 이벤트 관리

실제 처리:
  - 4 스레드가 1만 연결 모니터
  - CPU 캐시 효율 ↑
  - 컨텍스트 스위칭 최소
  
처리량:
  - 일반적으로 Blocking 보다 높음
  - 최대 처리량의 90~95%
  
장점:
  - 메모리 1/100
  - CPU 효율
  - 안정성

7.4 1만 연결 — Virtual Threads (Java 21+)

Virtual Threads + 8 OS 스레드:

자원:
  - OS 스레드: 8 (CPU 코어)
  - 가상 스레드: 1만 (각 1KB ~ 8KB)
  - 메모리: 약 50MB

처리:
  - 가상 스레드 = 1 연결
  - Blocking I/O 시 unmount → 다른 가상 스레드
  - 동기 코드 그대로
  
처리량:
  - Non-blocking 과 비슷
  - 코드는 단순
  
장점:
  - 가독성 ↑
  - 디버깅 쉬움
  - 효율 ↑

7.5 실제 측정 (예시)

시나리오: 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초
  - 코드: 동기, 단순

7.6 실무 적용

시나리오별 권장:

1. 모니터링/분석 시스템 (자주 호출):
   - Reactive 또는 Loom
   - 응답 시간 일관성

2. 일반 비즈니스 API:
   - Tomcat + Blocking
   - 사용자 수가 적당 (< 1천)

3. 대규모 이벤트 처리:
   - Netty 또는 Reactive
   - 초당 1만+ 요청

4. 미래 시스템:
   - Java 21+ Virtual Threads
   - 코드 단순 + 효율

7.7 ILIC 의 시나리오

ILIC 의 실제 시나리오:

평소 (100 동시):
  - Tomcat + Blocking 충분
  - 200 스레드 풀로 OK

피크 (1,000 동시):
  - Tomcat 한계 시작
  - Connection Pool 증대
  - Cache 활용
  - 또는 Reactive 전환

블랙 프라이데이 (10,000 동시):
  - Reactive 또는 Loom 필요
  - 또는 수평 확장
  - CDN, Load Balancer

향후 (Java 21+):
  - Virtual Threads 채택
  - 기존 코드 거의 그대로
  - 효율 대폭 향상

7.8 자기 점검 답변

1만 동시 연결의 처리는?

:
1. Blocking:

  • 1만 스레드 = 10GB
  • 비현실적
  • 컨텍스트 스위칭 폭발
  1. Non-blocking + Selector:

    • 4 스레드로 처리
    • 100MB
    • 효율적
  2. Reactive:

    • 비슷한 자원
    • 함수형 코드
  3. Virtual Threads (Java 21+):

    • 1만 가상 스레드
    • 8 OS 스레드
    • 동기 코드 + 효율
    • 미래 표준

8️⃣ 실무 적용 (Tomcat/Netty/WebFlux/Loom)

8.1 Tomcat 의 동시성

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

8.2 Netty 의 동시성

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
  - 대규모 동시성 서비스
  - 자체 프로토콜

8.3 Spring WebFlux

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()));

8.4 Project Loom (Java 21+)

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억 가상 스레드도 가능 (메모리 한도 내)

8.5 Virtual Threads 사용

// 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) {
            // 처리
        }
    }
}

8.6 Spring Boot + Virtual Threads

# Java 21 + Spring Boot 3.2+
spring:
  threads:
    virtual:
      enabled: true   # ★ Virtual Threads 활성화

# 효과:
# - Tomcat 의 워커 스레드가 가상 스레드로
# - @Async 도 가상 스레드로
# - 코드 변경 없이 효율 ↑

8.7 모델 선택 가이드

2024+ 권장:

1. Java 21+ 사용 가능?
   - YES → Virtual Threads 활성화
   - 코드 단순 + 효율
   
2. Java 17 사용?
   - Spring WebFlux 고려
   - 또는 일반 Tomcat (작은 규모)

3. 대규모 동시성 필수?
   - Reactive 또는 Virtual Threads
   - 절대 Blocking + 적은 스레드 풀 X

4. 레거시?
   - Tomcat + 적절한 스레드 풀
   - Connection Pool, Cache 활용

8.8 Non-blocking 이 항상 좋은가?

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)

8.9 ILIC 의 추천

// 권장 패턴 (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 활성화
// 효율 대폭 향상

8.10 자기 점검 답변

Tomcat/Netty/WebFlux/Loom 의 동시성 모델은?

:
1. Tomcat (NIO):

  • Selector 로 acceptor
  • 워커 스레드는 Blocking
  • 하이브리드
  1. Netty:

    • 순수 Non-blocking
    • EventLoopGroup + EventLoop
    • 한 스레드 + Selector
  2. Spring WebFlux:

    • Reactive (Mono/Flux)
    • Netty 기반
    • 백프레셔 지원
  3. Project Loom (Java 21+):

    • Virtual Threads
    • 동기 코드 + 효율
    • 미래 표준

9️⃣ Phase 7 졸업 시험 + 완주

9.1 Phase 7 졸업 시험 — 50 문항

I/O 기본 (10 문항)

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)

IO vs NIO (10 문항)

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, 명확한 예외

NIO.2 (10 문항)

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

Blocking/Non-blocking (10 문항)

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

종합 (10 문항)

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 자유자재 + 동시성 모델 선택

9.2 채점

50 / 50 → Phase 7 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 7.1 ~ 7.4 재학습

9.3 Phase 7 학습 종합

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

9.4 Phase 7 마스터 후 가능한 일

1. I/O 모델 선택
   - 시나리오에 맞는 모델
   - 자원 사용 분석

2. Tomcat 설정
   - 스레드 풀
   - Connection Pool
   - 최적화

3. Spring WebFlux 활용
   - Reactive 프로그래밍
   - Mono/Flux

4. 면접 자신감
   - 모든 I/O 질문
   - 동시성 모델
   - C10K 문제

5. Java 21+ 활용
   - Virtual Threads
   - 동기 코드 + 효율
   - 미래 표준

9.5 3주차 누적 진행

✅ 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%)

🎯 핵심 요약 — 3줄 정리

1. Blocking vs Non-blocking

  • Blocking: 호출 시 스레드 정지, 1 연결 = 1 스레드
  • Non-blocking: 즉시 리턴, 한 스레드가 다수 처리
  • 핵심 차이: 자원 사용 (메모리, 컨텍스트 스위칭)

2. Selector 의 본질

  • OS 의 epoll/kqueue/IOCP 의 자바 추상화
  • 한 스레드가 수많은 채널 모니터
  • C10K 문제의 해법

3. 동시성 모델 4가지

  • Thread-per-connection: 단순, 작은 규모
  • Event Loop (Netty): 대규모, 복잡
  • Reactive (WebFlux): 함수형, 백프레셔
  • Virtual Threads (Loom): 동기 코드 + 효율, 미래

📚 다음으로...

Unit 7.5 — 오버헤드와 File 객체

이번 Unit에서 동시성 모델을 봤다면, 다음은 오버헤드와 File 객체 — Phase 7 의 마지막.

  • 오버헤드의 정의
  • File 객체의 정밀 (createNewFile, getAbsolutePath 등)
  • 디렉토리 자동 생성 안 됨의 함정
  • Phase 7 마지막 정리

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 완주)
profile
Software Developer

0개의 댓글