옌의 로그

[Reactive Programming] Blocking I/O와 Non-Blockin I/O 본문

스터디/기타

[Reactive Programming] Blocking I/O와 Non-Blockin I/O

dev-yen 2025. 9. 25. 23:08

 

본 포스팅은 스프링으로 시작하는 리액티브 프로그래밍
책을 참고하여 작성하였습니다. ( _ _)

 

Blocking I/O

I/O란?

I/O(Input/Output)는 컴퓨터 시스템이 외부 세계(사용자, 디스크, 네트워크, 다른 프로그램 등)와 데이터를 주고받는 모든 과정을 뜻한다. 쉽게 말하면, "프로그램이 자기 안에서 계산만 하는 게 아니라, 밖과 소통하는 행위".

  • Input: 외부에서 데이터를 받아오는 것
    예) 키보드 입력, 파일 읽기, 네트워크 요청 수신
  • Output: 데이터를 외부로 내보내는 것
    예) 화면 출력, 파일 쓰기, 네트워크 응답 전송

즉, System.out.println("Hello") 같은 콘솔 출력도 I/O고, DB 쿼리 실행도 I/O

 

 

gpt가 만들어준 Blocking I/O 그래프 ㅋㅋ

만약 client가 서버에 데이터 요청을 했을 때, 서버가 추가 데이터를 받아오기 위해 외부 API 요청을 하게 되면, 처음 클라이언트 요청을 받았던 서버 스레드는 외부 응답이 돌아올 때 까지 대기 상태가 된다. 

>> 이처럼 하나의 스레드가 I/O에 의해 차단되어 대기하는 것을 Blocking I/O라 한다

 

멀티스레딩 ?

Blocking I/O 방식의 묹점을 보완하기 위해 추가 스레드를 할당하여 차단된 그 시간을 효율적으로 사용할 수 있다. 하지만, CPU 대비 많은 수의 스레드를 할당하는 멀티스레딩 기법은 몇 가지 문제점이 존재한다.

  • 컨텍스트 스위칭 (Context Switching)으로 인한 스레드 전환 비용 발생
    • 프로세스의 정보를 PCB에 저장, reload하는 시간 동안에는 CPU가 다른 작업을 하지 못하고 대기
    • 컨텍스트 스위칭이 많을수록 CPU 전체 대기 시간이 길어지기 때문에 성능저하
  • 과다한 메모리 사용으로 오버헤드 발생
    • 새로운 스레드가 실행되면 JVM이 해당 스레드를 위한 스택 영역 일부 할당, 새로운 스레드의 정보는 스택 영역에 개별 프레임의 형태로 저장
    • 일반적으로 서블릿 컨테이너 기반의 Java 웹 애플리케이션은 요청당 하나의 스레드를 할당
  • 스레드 풀(Thread Pool)에서 응답 지연이 발생
    • 스레드 풀이란 일정 개수의 스레드를 미리 생성해서 풀에 저장해두고 요청이 들어올 때, 아직 사용되지 않고 있는 스레드를 풀에서 꺼내서 사용할 수 있게 하는 스레드 저장소이다
    • 대량 요청이 발생해 스레드 풀에 사용 가능한 유휴 스레드가 없을 경우, 사용 가능한 스레드가 확보되기 전까지 응답 지연이 발생.
      -> 응답 지연에는 반납된 스레드가 사용 가능하도록 전환되는 지연 시간이 포함됨

 

 

Non-Blocking I/O

Blocking I/O와 반대로 스레드가 차단되지 않는다

gpt가 만들어준 Non-Blocking I/O 그래프

Blocking I/O와 다르게, 

 

  • 서버 스레드는 외부 API 요청을 보낸 뒤 즉시 해제되어 다른 작업을 계속 수행할 수 있고,
  • 응답이 도착하면 콜백/리스너를 통해 결과를 처리
  • 따라서 같은 하드웨어에서 더 많은 클라이언트 요청을 동시에 처리할 수 있다

 

Blocking I/O 방식보다 더 적은 수의 스레드를 사용하기 때문에 Blocking I/O의 멀티스레딩 기법을 사용할 때 발생한 문제점들이 생기지 않는다. 따라서 CPU 대기 시간 및 사용량에 있어서도 대단히 효율적이다.

But?

  • 스레드 내부에 CPU를 많이 사용하는 작업이 포함된 경우, 성능에 악영향
  • 사용자의 요청에서 응답까지의 전체 과정에 Blocking I/O 요소가 포함된 경우, Non-Blocking의 이점을 발휘하기 어려움

 

 

 

Spring Framework에서 Blocking I/O, Non-Blocking I/O

많이 사용되는 Spring MVC의 경우, Blocking I/O 방식을 사용한다.

Non-Blocking I/O 방식을 적용해 새로 나온 것이 Spring WebFlux.

  • 서블릿 컨테이너 기반의 Spring MVC는 요청당 하나의 스레드를 사용하기 때문에 대량의 요청을 처리하기 위해서 과도한 스레드를 사용함으로써 CPU 대기 시간이 늘어나고 메모리 사용 시 오버헤드가 발생한다.
  • Spring WebFlux는 Netty 같은 비동기 Non-Blocking I/O 기반의 서버 엔진을 사용함으로써 적은 수의 스레드로 많은 수의 요청을 처리하기 때문에 CPU와 메모리를 효율적으로 사용할 수 있어 적은 컴퓨팅 파워로 고성능의 애플리케이션을 운영할 수 있게 해준다.

 

Blocking 예시 코드 (spring mvc를 사용했다면 익숙한 구조)

import org.springframework.web.client.RestTemplate;

public class BlockingRestTemplateExample {
    public static void main(String[] args) {
        System.out.println("요청 시작");

        RestTemplate restTemplate = new RestTemplate();

        // 외부 API 호출 (Blocking)
        String url = "https://jsonplaceholder.typicode.com/todos/1";

        // exchange()나 getForObject() 둘 다 Blocking
        String response = restTemplate.getForObject(url, String.class);

        // 여기서 응답이 도착할 때까지 현재 스레드가 BLOCKED
        System.out.println("응답 수신 완료: " + response);
    }
}

 

  • getForObject() 호출 시 응답이 오기 전까지 스레드가 대기
  • 단순하고 직관적이지만, 많은 요청이 몰리면 스레드 자원 낭비
  • Spring 5 이후 deprecated 예정, 더 이상 권장되지 않음

 

 

Non-Blocking 예시 (spring webflux 기반)

import org.springframework.web.reactive.function.client.WebClient;

public class WebClientExample {
    public static void main(String[] args) {
        WebClient client = WebClient.create("https://jsonplaceholder.typicode.com");

        client.get()
                .uri("/todos/1")
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(response -> {
                    System.out.println("응답 수신 완료: " + response);
                });

        System.out.println("메인 스레드는 즉시 반환 → 다른 작업 가능");

        try { Thread.sleep(2000); } catch (InterruptedException e) {}
    }
}

 

  • 요청 후 스레드가 바로 반환 → 다른 요청/작업을 처리 가능
  • 응답이 오면 subscribe 콜백 실행
  • 고성능/대규모 동시처리에 적합
  • Spring 5+에서 공식적으로 권장되는 방식

 

Non-Blocking I/O (WebFlux) 도입 시 고려할 사항

Non-Blocking 방식이 성능이 더 뛰어나면 무조건 도입하면 되는거 아닐까? 라고 생각한다면 오산이다. (경기도 오산 ㅋㅅㅋ)

 

오해: "Non-Blocking = 무조건 성능 향상"

  • Non-Blocking 방식은 항상 더 빠른 것이 아님
  • 작은 트래픽이나 단순 시스템에서는 오히려 복잡도만 증가
  • Blocking I/O + 스레드 풀 튜닝으로도 충분히 감당 가능한 경우가 많음

 

도입 전 준비해야 할 부분

  1. 학습 난이도
    • 리액티브 스트림 개념(Flux, Mono, backpressure 등) 이해 필요
    • 디버깅/모니터링 난이도 ↑ (스택트레이스 단순하지 않음)
  2. 개발 인력 확보
    • 리액티브 프로그래밍 경험이 있는 개발자 필요
    • 코드 작성/리뷰/운영에 있어 사고방식 전환 요구 (imperative → reactive)
  3. 인프라 및 모니터링 체계
    • 기존 APM/모니터링 툴은 Blocking 기반 가정이 많음 → 적절한 모듈 선택 필요
    • 예: 스레드 기반 지표 대신 이벤트 루프/큐 기반 모니터링 준비

 

WebFlux 도입을 고려할 만한 상황

  1. 대량의 요청 트래픽이 발생하는 시스템
    • 수천~수만 동시 접속자를 처리해야 하는 경우
    • 요청당 I/O 대기 시간이 길어 스레드 효율을 높여야 할 때
  2. 마이크로서비스(MSA) 아키텍처
    • 서비스 간 호출이 빈번 → 네트워크 I/O 비중이 높음
    • Non-Blocking을 활용해 리소스 효율성 ↑
  3. 스트리밍/실시간 시스템
    • SSE(Server-Sent Events), WebSocket, Kafka 소비자 등
    • 지속적으로 데이터가 흐르는 구조에서 Reactive 모델이 자연스럽게 매칭됨

 

즉,,

  • 단순 CRUD API, 트래픽이 크지 않은 내부 서비스 → RestTemplate/Blocking I/O도 충분
  • 고트래픽, MSA, 실시간 처리 요구가 맞아떨어질 때 → WebFlux 적극 고려

 

 

마무리하며,,,

회사가 계속 MSA, MSA 노래를 부르는데 결국 MSA된 건 하나두 없다. 하지만 난 언젠가 겪을 MSA를 위해 Non-Blocking I/O를 열심히 공부한다~~ ^ㅁ^

 

Comments