개요
- 리액티브 스트림즈는 데이터 스트림을 Non-Blocking이면서 비동기적인 방식으로 처리하기 위한 리액티브 라이브러리의 표준 사양입니다.
- 따라서 Blocking 과 Non-Blocking 이 무엇을 뜻하는지 알아봅니다.
I/O
- I/O는 입력(Input)/출력(Output)의 약자로 운영체제에서 I/O는 일반적으로 컴퓨터 시스템이 외부 입출력 장치들과 데이터를 주고 받는 것을 의미합니다.
- I/O 작업의 예를 들면 디스크에 저장된 프로그램 실행파일을 읽어 메모리에 올리는 것을 들 수 있습니다.
- 웹 애플리케이션에서의 I/O 작업으로는 파일의 데이터를 읽는 것, 파일에 데이터를 기록하는 것, 데이터베이스에서 데이터를 조회하거나 추가하는 것, 다른 웹 애플리케이션으로 네트워크 통신을 하는 것 등이 있습니다.
Blocking I/O
- 위의 그림에서는 네트워크 I/O의 Blocking I/O를 나타냅니다.
- 클라이언트와 서버1, 서버2, 서버3이 있다고 가정합니다. 클라이언트 PC에서 서버1에 요청을 보내면 서버1은 서버2와 서버3에 요청을 보냅니다.
- 이 때 서버1에서 서버2로 요청을 보내는 시점에 서버1에서 실행된 스레드는 차단되어 서버2의 스레드가 끝나 응답을 반환하기 전까지 대기합니다.
- 서버2의 응답이 반환되면 서버1에서 차단된 스레드는 다시 실행되어 서버3으로 요청을 보내고 응답이 반환될 때 까지 서버1의 스레드는 다시 차단됩니다.
- 이렇게 하나의 스레드가 I/O에 의해 차단되어 대기하는 것을 Blocking I/O라고 합니다.
- Blocking I/O 방식의 문제점을 보완하기 위해서 멀티스레딩 기법으로 추가 스레드를 할당하여 차단된 시간을 효율적으로 사용할 수 있습니다.
멀티스레딩
- CPU 대비 많은 수의 스레드를 할당하는 멀티스레딩 기법은 몇 가지 문제점이 존재합니다.
- 컨텍스트 스위칭으로 인한 스레드 전환 비용이 발생합니다.
- 멀티스레딩은 하나의 CPU에서 여러 스레드를 번갈아 가면서 실행시켜 주는 것입니다.
- 번갈아 가며 실행될 때 기존에 실행되고 있는 스레드 정보를 Thread Control Block에 저장하고, 다시 실행시켜야 할 스레드 정보를 Thread Control Block 으로부터 불러오는 과정을 컨텍스트 스위칭이라고 합니다.
- 컨텍스트 스위칭을 할 때 TCB에 저장하고 불러오는데 일정 시간이 소요되므로 컨텍스트 스위칭이 많을수록 성능이 저하됩니다.
- 과다한 메모리 사용으로 오버헤드가 발생할 수 있습니다.
- 새로운 스레드가 실행되면 해당 스레드를 위한 스택 영역이 할당됩니다. 따라서 스레드가 늘어날수록 메모리 사용량이 늘어납니다.
- 스레드 풀(Thread Pool)에서 응답 지연이 발생할 수 있습니다.
- 스프링 부트의 내장 톰캣은 스레드 풀을 사용합니다.
- 스레드 풀은 일정 개수의 스레드를 미리 생성해서 풀에 저장해두고 요청이 들어왔을 때 사용되지 않는 스레드가 있으면 풀에서 꺼내 사용하는 일종의 스레드 저장소입니다.
- 많은 요청이 발생하여 스레드 풀에 사용가능한 스레드가 없을 경우 스레드가 확보되기 전까지 응답 지연이 발생합니다.
Non-Blocking I/O
- 위의 그림에서는 네트워크 I/O의 Non-Blocking I/O를 나타냅니다.
- Blocking I/O 와 반대로 스레드가 차단되지 않습니다.
- Blocking I/O 와 마찬가지로 클라이언트 PC에서 서버1에 요청을 보내면 서버1은 서버2와 서버3에 요청을 보냅니다.
- Blocking I/O 에서는 서버1에서 서버2로 요청을 보내면 응답이 오기전까지 스레드가 차단되어 응답이 반환될 때 까지 서버3으로 요청을 보낼 수 없었습니다.
- 하지만 Non-Blocking I/O 에서는 서버2로의 요청을 처리하는 동안 스레드가 차단되지 않아 바로 서버3으로 요청을 보낼 수 있습니다.
- 이처럼 Non-Blocking I/O 방식에서는 작업 스레드 종료 여부와 관계없이 요청 스레드는 차단되지 않습니다.
- 스레드가 차단되지 않기 때문에 하나의 스레드로 많은 수의 요청을 처리할 수 있습니다.
- 하지만 스레드 내부에 CPU를 많이 사용하는 작업이 포함된 경우에 성능에 악영향을 줄 수 있습니다.
- 또한 사용자의 요청에서 응답까지의 전체 과정에 Blocking I/O 요소가 포함된 경우 Non-Blocking I/O 의 이점을 발휘하기 힘듭니다.
Spring Framework에서의 Blocking I/O와 Non-Blocking I/O
- 기존의 Spring MVC 기반의 웹 애플리케이션에서는 Blocking I/O 방식을 사용합니다.
- 요청당 하나의 스레드를 사용하는 멀티스레딩 방식입니다.
- 대량의 요청을 처리하려면 과도한 스레드를 사용하므로 CPU 대기시간이 늘어나고 메모리 사용시 오버헤드가 발생합니다.
- Blocking I/O 방식의 문제점을 극복하기 위해 Spring MVC의 대안으로 나온 것이 Spring WebFlux 입니다.
- Spring WebFlux 는 Non-Blocking I/O 방식을 사용합니다.
- Netty 같은 비동기 Non-Blocking I/O 기반의 서버 엔진을 사용함으로써 적은 수의 스레드로 많은 수의 요청을 처리합니다.
- 따라서 CPU와 메모리를 효율적으로 사용하여 적은 컴퓨팅 파워로 고성능의 애플리케이션을 운영할 수 있습니다.
Blocking I/O와 Non-Blocking I/O 비교 예제
- Blocking I/O 와 Non-Blockng I/O 의 차이를 알아보기 위한 예제 프로젝트입니다.
- 프로젝트는 클라이언트와 서버1, 서버2로 구성되어있습니다.
- 기본적인 시나리오는 클라이언트에서 서버로 책의 ID를 파라미터로 API를 요청하면 해당 책의 제목과 내용 정보를 리턴합니다.
- 서버1에서는 두 개의 API가 제공됩니다.
- 서버1의 API가 호출되면 서버2의 API를 호출하여 책의 제목과 내용을 받아오는데 이 때 하나는 Blocking I/O로 처리하는 API, 하나는 Non-Blocking I/O로 처리하는 API 입니다.
- 서버2에서는 책의 제목을 리턴하는 API, 책의 내용을 리턴하는 API 가 제공됩니다.
먼저 서버 프로젝트의 의존성은 아래처럼 설정합니다. Spring WebFlux 를 사용하기 위해 spring-boot-starter-webflux
를 추가합니다.
아래는 서버1의 API 코드입니다.
/v1/books/{id}
는 Blocking I/O API 입니다.
- 서버1에서 서버2의 두 개의 API를 호출하여 네트워크 I/O가 발생하는데 이 때 사용하는
RestTemplate
은 Blocking I/O로 동작합니다. 따라서 첫 번째 API의 응답이 오기전까지 스레드가 차단됩니다.
- 하나의 API가 5초의 처리시간이 걸린다면 총 10초의 시간이 소요됩니다.
/v2/books/{id}
는 Non-Blocking I/O API 입니다.
WebClient
는 Non-Blocking I/O 으로 동작합니다. 따라서 첫 번쨰 API가 호출되고 바로 다음 API가 호출됩니다.
- 하나의 API가 5초의 처리시간이 걸린다면 약 5초의 시간이 소요됩니다.
아래는 서버2의 API 코드입니다.
- 각 API마다 약 5초의 시간이 걸린다고 시뮬레이션 하기위해
Thread.sleep(5000)
을 호출합니다.
아래는 클라이언트의 코드입니다. 클라이언트에서는 각 API를 호출하고 프로그램이 종료되기 위해 웹 서버로 동작하지 않도록 application.properties
파일에 spring.main.web-application-type=none
구문을 추가합니다.
- CommandLineRunner 인터페이스 구현체를 사용하여 프로그램 시작시 실행 할 코드를 추가합니다.
- 먼저 Blocking I/O API를 5번 호출하고, 그 이후 Non-Blocking I/O API를 5번 호출하였습니다.
- 이 때 각 API는
RestTemplate
을 사용하여 호출하여 각 API의 응답이 리턴되면 다음 API를 호출하도록 Blocking I/O로 동작하게 하였습니다.
- 실행 로그를 보면 Blocking I/O API는 하나의 API 응답에 10초의 시간이 걸리기 때문에 50초의 시간이 걸렸고, Non-Blocking I/O API는 하나의 API 응답에 5초의 시간이 걸리기 때문에 25초의 시간이 걸린 것을 볼 수 있습니다.
Non-Blocking I/O 방식의 통신이 적합한 시스템
- 대량의 요청 트래픽이 발생하는 시스템
- 요청 트래픽이 충분히 감당할 수준이라면 서블릿 기반의 Blocking I/O 방식의 애플리케이션으로 충분합니다.
- 대량의 요청 트래픽이 발생한다면 Spring WebFlux 기반 애플리케이션으로 전환을 고려해 볼 만 합니다.
- 서버 증설이나 VM 확장 등을 통해 트래픽 분산이 가능하지만 그만큼 높은 비용을 지불해야 합니다.
- 마이크로서비스 기반 시스템
- 마이크로서비스 기반 시스템은 특성상 서비스들 간에 많은 수의 I/O가 지속적으로 발생합니다. 따라서 서비스들 간의 통신에서 Blocking으로 응답 지연이 발생하게 된다면 다른 서비스들에도 영향을 미칠 가능성이 높습니다.
- 스트리밍 또는 실시간 시스템
- 리액티브 프로그래밍은 HTTP 통신이나 데이터베이스 조회 같은 일회성 연결 뿐만 아니라 끊임없이 들어오는 무한한 데이터 스트림을 전달받아서 효율적으로 처리할 수 있습니다.
예제 프로젝트
전체 코드는 https://github.com/nefertirii/spring-reactive-test/tree/fd9751f98982a70bb41a9a11afbb9698de998f9f 에서 확인하실 수 있습니다.