ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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)

    1. 요청: User Thread가 커널에 I/O 작업(예: read())을 요청하는 시스템 콜(System Call)을 수행합니다.
    2. Blocking: 커널은 데이터가 준비될 때까지 즉시 응답하지 않습니다. 이때 User Thread의 상태는 RUNNABLE에서 WAITING (또는 BLOCKED)으로 변경됩니다.
    3. Context Switch: OS 스케줄러는 해당 쓰레드를 CPU에서 내리고, 다른 쓰레드에게 CPU를 할당하는 문맥 교환(Context Switching)을 수행합니다.
    4. 완료 및 복귀: 디스크/네트워크 컨트롤러가 데이터 준비 완료 인터럽트(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)

    1. 요청: User Thread가 소켓을 Non-blocking 모드로 설정하고 read() 시스템 콜을 요청합니다.
    2. Non-blocking (즉시 반환): 커널은 데이터가 없으면 대기하지 않고 즉시 에러 코드(EAGAIN 또는 EWOULDBLOCK)를 반환합니다. 쓰레드는 중단되지 않습니다.
    3. Sync (Polling): User Thread는 제어권을 돌려받았지만, 원하는 데이터가 도착했는지 확인하기 위해 반복문(Loop)을 돌며 read()를 계속 호출합니다.
    4. 완료: 데이터가 준비된 시점에 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)

    1. 요청: User Thread는 커널에게 I/O 작업을 요청함과 동시에, "작업이 끝나면 실행할 콜백(Callback)"이나 이벤트를 등록합니다. (epoll_ctl 등 활용)
    2. Non-blocking: 커널은 요청을 접수하고 즉시 제어권을 반환합니다. User Thread는 다른 작업(다른 요청 처리 등)을 계속 수행합니다.
    3. 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)

    1. Async 요청: 작업을 별도의 쓰레드나 비동기 함수에 위임합니다.
    2. Blocking: 하지만 호출한 쓰레드가 결과값을 즉시 필요로 하여 Future.get()이나 CountDownLatch.await() 같은 함수를 호출합니다.
    3. 대기: 비동기 작업이 진행되는 동안, 호출한 쓰레드는 결국 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
Designed by MSJ.