-
[Java] I/O 모델의 4가지 조합Java 2025. 12. 1. 13:49
이번 글에서는 이 두 가지 축이 결합된 4가지 I/O 모델이 커널(Kernel)과 유저 애플리케이션(User Application) 사이에서 기술적으로 어떻게 상호작용하는지 분석합니다.
1. Synchronous + Blocking (동기 + 블로킹)
"가장 기본적인 I/O 모델"
Java의 전통적인 InputStream, JDBC 등이 작동하는 방식입니다. 호출자(Caller)는 I/O 작업이 완료될 때까지 쓰레드 자원을 점유한 채 대기합니다.
1-1. 동작 메커니즘 (System Flow)
- 요청: User Thread가 커널에 I/O 작업(예: read())을 요청하는 시스템 콜(System Call)을 수행합니다.
- Blocking: 커널은 데이터가 준비될 때까지 즉시 응답하지 않습니다. 이때 User Thread의 상태는 RUNNABLE에서 WAITING (또는 BLOCKED)으로 변경됩니다.
- Context Switch: OS 스케줄러는 해당 쓰레드를 CPU에서 내리고, 다른 쓰레드에게 CPU를 할당하는 문맥 교환(Context Switching)을 수행합니다.
- 완료 및 복귀: 디스크/네트워크 컨트롤러가 데이터 준비 완료 인터럽트(Interrupt)를 발생시키면, 커널은 데이터를 유저 공간으로 복사하고 쓰레드를 다시 깨웁니다(RUNNABLE). 이때 결과값과 함께 제어권이 반환됩니다.
1-2. Java 코드 예시
// read() 호출 시 시스템 콜 발생 -> 커널 모드 진입 -> 쓰레드 Block // 데이터가 유저 메모리에 복사될 때까지 라인이 넘어가지 않음 String data = inputStream.read(); // 데이터 수신이 완료되어야(Sync) 실행 재개 process(data);1-3. 기술적 특징
- 리소스: I/O 대기 시간 동안 쓰레드가 점유된 채 유휴(Idle) 상태가 되므로, 동시 요청이 많을 경우 Thread Pool 고갈(Exhaustion)이 발생합니다.
- 순서: 요청과 응답의 순서가 엄격하게 보장됩니다.
2. Synchronous + Non-blocking (동기 + 논블로킹)
"Busy Waiting을 유발하는 Polling 모델"
제어권은 즉시 회수하지만, 결과 완료 여부를 확인하기 위해 지속적으로 커널을 호출하는 방식입니다.
2-1. 동작 메커니즘 (System Flow)
- 요청: User Thread가 소켓을 Non-blocking 모드로 설정하고 read() 시스템 콜을 요청합니다.
- Non-blocking (즉시 반환): 커널은 데이터가 없으면 대기하지 않고 즉시 에러 코드(EAGAIN 또는 EWOULDBLOCK)를 반환합니다. 쓰레드는 중단되지 않습니다.
- Sync (Polling): User Thread는 제어권을 돌려받았지만, 원하는 데이터가 도착했는지 확인하기 위해 반복문(Loop)을 돌며 read()를 계속 호출합니다.
- 완료: 데이터가 준비된 시점에 read()를 호출하면, 커널은 데이터를 복사해주고 정상적인 바이트 수를 반환합니다.
2-2. Java 코드 예시
// 1. Non-blocking 호출 (즉시 리턴) Future<String> future = taskExecutor.submit(() -> ioTask()); // 2. Sync (Polling): 호출자가 주도적으로 완료 여부를 계속 확인 while (!future.isDone()) { // CPU를 점유하며 반복 확인 (Busy Waiting) Thread.sleep(10); } // 3. 결과 수령 String result = future.get();2-3. 기술적 특징
- 리소스: 쓰레드가 Block되지는 않으나, 의미 없는 루프를 돌며 시스템 콜을 반복하므로 CPU 사용률(Usage)이 급증하는 Busy Waiting 현상이 발생합니다.
- 지연: Polling 주기(Interval)에 따라 응답 지연(Latency)이 발생할 수 있습니다.
3. Asynchronous + Non-blocking (비동기 + 논블로킹)
"I/O Multiplexing 및 Event-Driven 모델"
현대적인 고성능 서버(Spring WebFlux, Node.js, Nginx)의 기반이 되는 모델입니다.
3-1. 동작 메커니즘 (System Flow)
- 요청: User Thread는 커널에게 I/O 작업을 요청함과 동시에, "작업이 끝나면 실행할 콜백(Callback)"이나 이벤트를 등록합니다. (epoll_ctl 등 활용)
- Non-blocking: 커널은 요청을 접수하고 즉시 제어권을 반환합니다. User Thread는 다른 작업(다른 요청 처리 등)을 계속 수행합니다.
- Async (Interrupt & Callback):
- 하드웨어로부터 데이터가 도착하면 커널은 이벤트를 발생시킵니다.
- I/O Multiplexing(Selector): 별도의 이벤트 루프 쓰레드가 완료 신호를 감지하고, 미리 등록된 콜백 함수를 실행하거나 Publisher를 통해 데이터를 방출합니다.
3-2. Java 코드 예시 (WebFlux)
// 1. 비동기/논블로킹 요청 webClient.get().uri("/data") .retrieve() .bodyToMono(String.class) // 2. Callback 등록: 결과가 도달했을 때 수행할 로직 정의 .subscribe(result -> handleSuccess(result), error -> handleError(error)); // 3. 메인 쓰레드는 I/O 대기 없이 즉시 다음 로직 수행 doOtherIndependentWork();3-3. 기술적 특징
- 리소스: Context Switching 비용이 최소화되며, 소수의 쓰레드(Event Loop)로 수만 개의 연결을 처리할 수 있습니다.
- 복잡도: 실행 흐름이 콜백이나 리액티브 체인으로 분산되어 디버깅(Stack Trace 추적)이 어렵습니다.
4. Asynchronous + Blocking (비동기 + 블로킹)
"비동기의 이점을 상쇄하는 안티 패턴 (Anti-Pattern)"
비동기로 작업을 시작했으나, 의도치 않게(혹은 기술적 한계로) 결과를 기다리며 대기하는 경우입니다.
4-1. 동작 메커니즘 (System Flow)
- Async 요청: 작업을 별도의 쓰레드나 비동기 함수에 위임합니다.
- Blocking: 하지만 호출한 쓰레드가 결과값을 즉시 필요로 하여 Future.get()이나 CountDownLatch.await() 같은 함수를 호출합니다.
- 대기: 비동기 작업이 진행되는 동안, 호출한 쓰레드는 결국 WAITING 상태로 전환되어 자원을 점유한 채 대기합니다.
4-2. Java 코드 예시
// 1. Async: 별도 쓰레드에 작업 위임 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> slowIoTask()); // ... 다른 작업을 할 수 있는 기회가 있었으나 ... // 2. Blocking: 결과를 얻기 위해 쓰레드 대기 (Blocking 발생) // 비동기 작업이 끝날 때까지 이 쓰레드는 멈춤 String result = future.get();4-3. 기술적 특징
- 비효율: 비동기 처리를 위한 오버헤드(객체 생성, 쓰레드 할당 등)는 발생하는데, 정작 호출 쓰레드는 멈춰버려 Sync-Blocking보다 성능이 떨어질 수도 있습니다.
- 발생 원인: 주로 레거시 코드와 최신 비동기 라이브러리를 혼용할 때나, 개발자의 실수로 발생합니다.
'Java' 카테고리의 다른 글
[Java] 멀티스레드의 위험과 동시성 제어 (1) 2025.12.01 [Java] Thread란? (0) 2025.12.01 [Java] Sync vs Async (0) 2025.12.01 [Java] Blocking/Non-blocking이란? (0) 2025.12.01 [Java] 프로세스(Process)와 스레드(Thread) (1) 2025.11.27