본문 바로가기

도서/스프링으로하는 마이크로서비스 구축

Spring Reactor - 동기 vs 비동기, 블로킹 I/O vs 논블로킹 I/O

 이번 장의 주제는 리액티브 마이크로서비스로, 중요하게 다룰 주제는 크게 두 가지로 다음과 같습니다. 

  1. Spring Reactor - 동기 vs 비동기, 블로킹 I/O와 논블로킹 I/O
  2. Event Driven System

두 주제가 모두 내용이 방대하고 난이도가 있어서 포스팅을 두 개로 나눌 예정입니다. 첫 번째 주제인 Spring Reactor - 동기 vs 비동기, 블로킹 I/O와 논블로킹 I/O 에 대해 학습한 내용을 정리해보려고 합니다.


리액티브 마이크로서비스 

리액티브 마이크로서비스란, 리액티브 프레임워크를 적용한 마이크로서비스를 의미합니다. 리액티브 프레임워크는 논블로킹(Non-Blocking) + 비동기 API 기술을 사용하며 사용자의 반응성을 중시하는 프레임워크를 의미합니다. 

 

논블로킹과 비동기에 대한 설명을 하기 전에 리액티브 프레임워크를 사용하는 것의 이점에 대해 먼저 소개하려고 합니다. 일반적으로 HTTP 기반의 RESTful API와 같은 블로킹 I/O 모델을 사용해 동기식 통신을 구현할 때는 명확한 단점이 존재합니다. (현 설명에서는 동기, 비동기, 블로킹, 논블로킹을 안다고 가정하고 포스팅하고, 이후에 관련 개념에 대해 자세히 설명하려고 합니다.)

 

블로킹 I/O + 동기식 통신은 요청을 처리하는 동안 운영체제가 스레드를 점유하는 시간이 오래 걸려, 동시 요청수가 증가하거나 요청과 관련된 컴포넌트가 증가하면 운영체제의 가용스레드가 부족해 응답시간이 늦어지거나 서버가 중단되는 문제가 발생할 수 있습니다. 

 

요약하자면 블로킹 I/O + 동기식 통신은 사용자가 즉각적인 반응을 보려고 하는 경우에서는 효용성이 떨어질 수 있습니다. 그럼 이제부터 동기와 비동기, 블로킹과 논블로킹의 차이를 보면서 어떻게 리액티브 프레임워크는 위의 문제를 해결했는지 정리하겠습니다. 


동기 vs 비동기

동기와 비동기는 작업 목록을 순차적으로 처리하는지 아닌지에 따라 구별됩니다. 동기식 처리는 작업을 순차적으로 처리하며 이전 작업이 처리된 후에 이후 작업을 수행하기 때문에 단순하고 명확한 대신, 대기하는 작업 목록이 커질수록 전체 작업 시간이 길어진다는 단점이 존재합니다. 

 

비동기는 순차적으로 작업을 처리하지 않습니다. 비동기는 작업을 시작한 후에 해당 작업이 끝날 때까지 대기하지 않으며 다른 작업을 바로 수행할 수 있습니다. 때문에 먼저 요청한 작업이 이후에 요청한 작업들보다 나중에 종료될 수 있어 작업의 종료 시점을 예측할 수 없는 대신, 전체 작업시간이 단축된다는 장점이 존재합니다. 

 

 

위 이미지처럼 동기식은 왼쪽 그림처럼 순차적으로 작업을 처리하며 비동기식은 오른쪽처럼 여러 작업이 거의 동시에 수행될 수 있습니다. 1번 작업이 2번 작업 보다 0.1초 정도 먼저 수행됐지만, 2번 작업이 먼저 끝나는 상황은 비동기 처리에서는 가능합니다. 

 

그럼 어떻게 비동기 처리가 가능한 걸까요? 동기식 처리는 이전 작업이 종료된 시점에서 후행 작업들을 동작시키고 마지막 작업이 종료된 시점에서 전체 작업이 종료됐다고 판단할 수 있습니다. 직관적으로 이해하기 편하다고 할 수 있죠. 비동기는 그럼 전체 작업이 끝나는 시점을 어떻게 알 수 있을까요? 

 

비동기 방식은 Event Loop와 Event Queue가 전체 작업에 관여하면서 작업의 종료 시점을 알 수 있도록 기능합니다. 

먼저, Event Queue는 비동기 작업을 위한 콜백 함수들이 저장되는 큐를 의미합니다. 여기서 말하는 콜백함수는 작업이 종료됐을 때 호출되는 함수를 의미합니다. 비동기 작업이 완료되면 해당 적업의 콜백 함수가 이벤트 큐에 들어가게 됩니다. 

Event Loop는 이벤트 큐에 들어온 콜백함수를 순차적으로 실행하는 역할을 담당합니다. 즉, 비동기는 Event Queue에 들어온 콜백 함수가 전부 실행되는 시점에서 전체 작업이 종료됐음을 알 수 있습니다. 


블로킹 I/O vs 논블로킹 I/O

블로킹 I/O와 논블로킹 I/O는 작업 중 대기 여부에 대한 개념입니다. 블로킹 I/O는 선행작업이 종료될 때까지 대기하고 논블로킹은 대기하지 않습니다. 즉, 논블로킹은 작업을 병렬적으로 처리함을 의미합니다. 

 

 

블로킹과 논블로킹의 관계도 동기와 비동기를 설명할 때 보여준 이미지를 통해 이해할 수 있습니다. 블로킹 I/O는 작업이 순차적으로 수행된 느 반면에 비동기는 전체 작업이 병렬적으로 수행됨을 알 수 있습니다. 


비동기와 논블로킹 I/O의 관계

저는 비동기와 논블로킹 I/O의 개념상 차이를 이해하기가 어려웠던 것 같습니다. 결과만 놓고 보면 두 개념이 동일한 의미전달을 하는 것 같아 보이고 저는 심지어 비동기가 논블로킹의 상위 개념이 아닌가? 하는 생각을 했던 것 같습니다. 

 

저는 두 개념을 혼동하지 않기 위해서 동기와 비동기 뒤에  '작업'이라는 수식어를 붙이고, 블로킹과 논블로킹 I/O 뒤에 '방식'이라는 수식어를 붙여 이해했습니다. 

 

비동기적 '작업'이 논블로킹 '방식'으로 구현될 수 있습니다.

 

예를 하나 들어보려고 합니다. 비동기적으로 파일을 읽는 '작업'을 할 때, 파일 읽기 작업은 비동기적으로 요청되어 다른 '작업'과 동시에 진행될 수 있습니다. 동시에 파일이 읽히는 동안 다른 '작업'을 처리할 수 있도록 만들기 위해 논블로킹 '방식'을 사용할 수 있습니다. 

 

다만, 모든 비동기 작업이 논블로킹 '방식'으로 동작하는 것은 아닐 수 있습니다. 예를 들어 비동기적으로 데이터베이스에서 쿼리를 실행하 더러라도 데이터베이스의 응답이 블로킹되어 비동기적으로 '작업'을 요청했지만, 백그라운드에서는 블로킹 '방식'으로 '작업'을 처리하고 있기 때문입니다. 

 

이처럼 비동기와 논블로킹은 엄연히 다른 개념이지만 함께 했을 때 더욱 시너지를 발휘하는 관계로 이해할 수 있을 것 같습니다. 


Spring Reactor

다시 본론으로 돌아와서 일반적인 HTTP RESTFul API 통신은 블로킹 I/O를 사용해서 동기식 통신을 주고받는데, 운영체제가 스레드를 점유하는 시간이 오래 걸려, 동시 요청수가 증가하거나 요청과 관련된 컴포넌트가 증가하면 운영체제의 가용스레드가 부족해 응답시간이 늦어지거나 서버가 중단되는 문제가 발생할 수 있는 문제가 있다고 언급했었습니다. 

 

논블로킹 I/O 방식을 사용한 비동기식 통신 역시 운영체제에서 스레드를 점유하는 것은 마찬가지입니다. 다만, 각 스레드별로 점유되는 시간, 즉 대기 시간을 최소화함으로써 스레드 사용의 효율성을 높이는 것으로 이해하면 좋을 것 같습니다. 

 

이제는 Spring Reactor를 사용해서 리액티브 프레임워크를 설계하는 방법에 대해 정리하려고 합니다. Spring Reactor는 리액티브 프로그래밍을 지원하는 Spring 프레임워크의 모듈 중 하나로, 주로 논블로킹 방식을 사용해서 비동기적 작업을 처리할 수 있게 해주는 기술입니다. spring reactor에 대한 사용예제를 보기 전에 주요 개념에 대해 먼저 정리하려고 합니다. 

  • 데이터를 스트림으로 처리한다는 것의 의미 
  • Flux와 Mono

1. 데이터를 스트림으로 처리한다는 것의 의미 

 

데이터를 스트림으로 처리한다는 건, 데이터를 일정한 크기의 덩어리가 아닌 연속적인 데이터의 흐름으로 처리한다는 것입니다. 즉, 데이터를 조각내어 연속적으로 처리함으로써 데이터를 한 번에 메모리에 올려놓고 사용하는 것에 비해 메모리 효율적으로 처리한다고 이해할 수 있습니다. (아직 잘 이해되지 않을 수 있지만, 코드와 함께 아래에서 한 번 더 보충하려고 합니다.)

 

2. Flux와 Mono

 

Spring Reactor에서는 주로 두 가지 타입만을 사용합니다.(자세한 설명은 역시 코드와 함께 보충하겠습니다.)

  • Flux : 0부터 N개의 요소를 발행할 수 있는 리엑티브 스트림
  • Mono : 0개 또는 1개의 요소만을 발행할 수 있는 리액티브 스트림 

 


Spring Reactor 실습 with Spring WebFlux

1. build.gradle

dependencies {
	implementation('org.springframework.boot:spring-boot-starter-webflux')
	implementation('io.springfox:springfox-spring-webflux:3.0.0-SNAPSHOT')
	testImplementation('io.projectreactor:reactor-test')
}

 

Spring Reactor 관련 의존성을 추가합니다. 위에서 언급하지 않은 개념 중 Spring WebFlux에 대한 의존성이 함께 추가됐음을 알 수 있습니다. Spring WebFlux는 리액티브 웹 애플리케이션을 개발할 수 있게 해주는 모듈로 비동기 및 논블로킹 웹 개발을 지원합니다. 

 

Spring Reactor는 주로 리액티브 스트림 처리를 지원하는 라이브러리로써 역할하고, Spring WebFlux는 이를 활용해서 리액티브 웹 애플리케이션을 개발할 수 있게 해주는 프레임워크로 보면 될 것 같습니다. 

 

2. Flux와 Mono로 데이터 타입 반환

public Flux<Recommendation> getRecommendations(int productId) {

    return repository.findByProductId(productId)
        .log()
        .map(e -> mapper.entityToApi(e))
        .map(e -> {e.setServiceAddress(serviceUtil.getServiceAddress()); return e;});
}

 

 

리액티브 프레임워크의 특징 중 하나는 선언적 처리라는 것입니다. 해당 메서드의 동작 방식에 대해 자세히 보면서 선언적 처리의 의미를 이해해보려고 합니다. 

  • 클라이언트의 http 요청이 들어옵니다. 
  • Spring WebFlux는 요청을 받고 해당 요청을 처리할 getRecommendations() 메서드를 호출합니다.
  • 해당 메서드는 Flux를 생성하여 반환합니다. 이 Flux는 데이터를 비동기적으로 발행할 수 있는 리액티브 스트림으로 데이터를 처리하는 방식을 정의할 뿐 실제 데이터를 즉시 가져오거나 반환하지는 않습니다.
  • 클라이언트의 요청에 따라 Spring WebFlux는 반환된 Flux를 구독합니다. Flux가 데이터를 발행하고 처리하는 시작점으로써 구독이 시작되면 실제 데이터 처리 과정이 시작되는 것을 의미합니다. 
  • Flux가 구독되면 Spring WebFlux는 실제 데이터를 조회하는 데이터베이스 쿼리를 실행합니다. 
  • 데이터베이스에서 가져온 결과는 데이터 변환 로직 처리를 겹쳐서 Flux에 의해 발행됩니다. Flux는 데이터를 비동기적으로 클라이언트에게 반환하며, 클라이언트는 이 데이터를 수신하고 처리할 수 있습니다. 
public Mono<ProductAggregate> getCompositeProduct(int productId) {
    return Mono.zip(
        values -> createProductAggregate((Product) values[0], (List<Recommendation>) values[1], (List<Review>) values[2], serviceUtil.getServiceAddress()),
        integration.getProduct(productId),
        integration.getRecommendations(productId).collectList(),
        integration.getReviews(productId).collectList())
        .doOnError(ex -> LOG.warn("getCompositeProduct failed: {}", ex.toString()))
        .log();
}

 

위 코드처럼 getRecommendations를 호출하는 것처럼, product, review도 각각 호출하고 있음을 알 수 있습니다. 각 호출에 의해서 데이터를 return 받는 시점은 product를 먼저 호출했다고 해서 제일 먼저 반환받기를 기대하기는 어렵습니다. 작업이 비동기적으로 수행되기 때문입니다. 각 요청에 대한 응답이 모두 왔을 때 zip함수로 작업을 합쳐서 Mono 객체를 반환하고 있음을 코드에서 확인할 수 있습니다. 

 

리액티브 프레임워크의 장점은 product 작업의 응답이 길어질 때 뒤의 후행 작업의 대기시간이 길어졌을 때 효과를 발휘할 수 있습니다. 후행작업이 대기하지 않고 바로 작업을 시작함으로써 전체 작업의 대기시간을 줄여줄 수 있고, 이는 사용자 대기시간을 줄일 수 있다는 것을 의미합니다. 

 

 

반응형