📗 BOOK/스프링으로 시작하는 리액티브 프로그래밍

3장 Blocking I/O vs Non-Blocking I/O

미미누 2024. 11. 20. 22:42

Blocking I/O

  • 하나의 스레드가 I/O에 의해서 차단되어 대기하는 것

CPU 대비 많은 수의 스레드를 할당하는 멀티스레드는 문제점 발생

  • 컨텍스트 스위칭으로 인한 스레드 전환 비용 발생
    • 컨텍스트 스위칭
      • 기존에 실행되고 있는 프로세스의 정보를 PCB라는 공간에 저장되고, 다시 실행해야 할 프로세스 정보를 PCB로부터 불러오는 과정을 컨텍스트 스위칭이라고 한다.
      • 컨텍스트 스위칭이 많으면 많을수록 CPU 전체 대기 시간은 길어지기 때문에 성능은 저하된다.
  • 과도한 메모리 사용으로 오베헤드 발생 가능성
    • 새로운 스레드가 생성되면 JVM에서는 해당 스레드를 위한 스택 영역의 일부를 할당하며, 스래드 정보는 개별 프레임에 저장
    • 서블릿 컨테이너 기반의 Java 웹 애플리케이션은 요청당 하나의 스레드를 할당한다. 각각 하나의 스레드 내부에서 스레드를 추가로 할당하게 된다면, 메모리 사용량 용량 높아질 가능성이 크다.
  • 스레드 풀에서 응답 지연 가능성
    • 톰캣에서는 스레드 풀 사용
    • 대량의 요청이 발생하게 되어 스레드 풀에 사용 가능한 유후 스레드가 없을 경우, 사용 가능한 스레드가 확보되기 전까지 응답 지연이 발생한다.

Non-Blocking I/O

  • Non-Blocking I/O 방식의 경우, 작업 스레드 종료 여부와 상관없이, 요청한 스레드는 차단되지 않는다.

Non-Blocking I/O의 단점

  • 만약 스레드 내부에 CPU를 많이 사용하는 작업이 포함되는 경우 성능에 악영향을 미친다.
  • 사용자의 요청에서 응답까지 전체 과정에서 Blocking I/O 요소가 포함된 경우에는 Non-Blocking의 이점을 누리기 힘들다.

Spring Framework에서의 Blocking I/O와 Non-Blocking I/O

  • Spring MVC 기반의 웹 애플리케이션이 Blocking I/O 방식을 사용한다.
    • 이러한 문제점을 극복하기 위해 나온 기술이 Spring WebFlux 이다.
    • 서블릿 컨테이너 기반의 Spring MVC는 요청당 하나의 스레드를 사용하기 때문에, 대량의 요청을 처리하기 위해서 과도한 스레드를 사용하기 때문에, 오버헤드 발생
    • Spring WebFlux는 Netty 같은 비동기 Non-Blocking I/O 기반의서버 엔진을 사용함으로써 적은 수의 스레드로 많은 수의 요청을 처리하기 때문에 CPU 와 메모리 를 효율적 사용 가능

Non-Blocking I/O 본사 API 서버

@Slf4j
@RestController
public class BookController {

    private final RestTemplate restTemplate = new RestTemplate();

    @GetMapping("/v1/books/{id}")
    public BookReadResponse getBookV1(@PathVariable String id) {

        String url = "<http://localhost:8081/books/>" + id + "/title";
        String title = restTemplate.getForObject(url, String.class);

        url = "<http://localhost:8081/books/>" + id + "/content";
        String content = restTemplate.getForObject(url, String.class);

        return new BookReadResponse(title, content);
    }

    @GetMapping("/v2/books/{id}")
    public Mono<BookReadResponse> getBookV2(@PathVariable String id) {
        String url = "<http://localhost:8081/books/>" + id + "/title";

        Mono<String> title = WebClient.create()
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);

        url = "<http://localhost:8081/books/>" + id + "/content";

        Mono<String> content = WebClient.create()
                .get()
                .uri(url)
                .retrieve()
                .bodyToMono(String.class);

        return Mono.zip(title, content)
                .map(tuple -> new BookReadResponse(tuple.getT1(), tuple.getT2()));
    }
}
@Slf4j
@RestController
public class BookController {

    @GetMapping("/books/{id}/title")
    public Mono<String> getBookTitle(@PathVariable String id) throws InterruptedException {
        Thread.sleep(5000);
        return Mono.just("Title " + id);

    }

    @GetMapping("/books/{id}/content")
    public Mono<String> getBookContent(@PathVariable String id) throws InterruptedException {
        Thread.sleep(5000);
        return Mono.just("Content " + id);
    }
}

Mono

  • Mono는 Reactor에서 지원하는 Publisher 타입 중 하나로, 단 하나의 데이터만을 emit하는 Publisher 타입이다. Json은 Mono를 사용하기 적합하다.
  • 본사 API를 반복적으로 호출하는 부분에서 subscribe()를 통해 응답 데이터를 전달받은 후 처리
    • 본사 API로부터 전달받은 응답이 Mono타입이고, Mono는 Reactor에서 지원하는 Publisher 타입이기 때문이다.

Blocking I/O의 특징

  • Non-Blocking I/O는 작업 스레드의 작업이 종료될 때까지 요청 스레드가 차단된다.
  • 멀티 스레딩 기법 사용 가능, 단 사용시 컨텍스트 스위칭 전환 비용, 메모리 사용 오버헤드, 스레드 풀 응답 지연 문제 발생 가능

Non-Blocking I/O의 특징

  • 작업 스레드의 종료 여부와 상관없이 요청 스레드가 차단되지 않는다.
  • 적은 수의 스레드만 사용하여, 스레드 전환 비용이 적으므로 CPU 효율적 사용 가능
  • CPU를 많이 사용하는 작업의 경우에는 성능에 악영향을 미칠 수 있다.

Spring Webflux를 언제 사용해야 할까

  • 대량의 요청 트래픽이 발생하는 시스템
    • 요청 트래픽이 충분히 감당할 수준이라면 서블릿 기반의 Blocking I/O 방식의 애플리케이션으로 충분합니다.
    • 대량의 요청 트래픽이 발생한다면 Spring WebFlux 기반 애플리케이션으로 전환을 고려해 볼 만 합니다.
    • 서버 증설이나 VM 확장 등을 통해 트래픽 분산이 가능하지만 그만큼 높은 비용을 지불해야 합니다.
  • 마이크로서비스 기반 시스템
    • 마이크로서비스 기반 시스템은 특성상 서비스들 간에 많은 수의 I/O가 지속적으로 발생합니다. 따라서 서비스들 간의 통신에서 Blocking으로 응답 지연이 발생하게 된다면 다른 서비스들에도 영향을 미칠 가능성이 높습니다.
  • 스트리밍 또는 실시간 시스템
    • 리액티브 프로그래밍은 HTTP 통신이나 데이터베이스 조회 같은 일회성 연결 뿐만 아니라 끊임없이 들어오는 무한한 데이터 스트림을 전달받아서 효율적으로 처리할 수 있습니다.