Base/개념정리

리액티브 프로그래밍

findmypiece 2022. 7. 19. 01:54
728x90

일단 블로킹에 대해 정확히 집고 넘어가자. 블로킹 상태는 스레드가 CPU 자원은 점유하고 있지만 무언가 일을 하는게 아니고 다른 무언가의 응답을 기다리는 대기상태를 의미한다.

 

예를 들어 수백번의 for문을 돌리는 로직을 수행하는 동안은 스레드가 열심히 일을 하고 있는 상태이기 때문에 블로킹 상태가 아니다. 일하는 만큼 CPU 자원을 사용하는 것이기 때문에 비효율도 없다.

 

반면, DB/API 통신과 같은 I/O 작업의 경우 스레드는 응답결과를 기다리는 동안 대기상태가 되고 일은 하지 않으면서 CPU 자원을 점유하고 있는 비효율이 발생한다.

 

또한 톰캣에서는 스레드풀 지정된 스레드를 만들어 놓고 재사용하는데 이러한 블로킹 상태가 오래 지속되는 스레드가 많아진다면 가용한 스레드 부족으로 요청을 제대로 처리하지 못하는 상황에 놓이게 된다.

 

그렇다고 스레드 갯수를 무한정 늘릴 수는 없다. 그만큼 CPU와 Memory 자원이 더 필요하고 어찌되었건 자원은 한정되어 있기 때문이다. 그리고 스레드가 많아지면 Context Switching 이 더 빈번하게 일어나고 자연스레 그 비용도 증가한다.

 

전통적인 MVC방식에서 이럴 때 대처하는 방식은 단순했다. 블로킹 시간이 오래 걸리지 않도록 DB 쿼리를 튜닝하거나 API를 개선했지만 하지만 어찌되었건 짧은 시간이라도 블로킹은 CPU 자원 사용의 비효율을 가져오기 때문에 보다 근본적인 해결책이 필요하다.

 

비동기-논블로킹 방식을 사용하면 블로킹 문제를 해결될 수 있을까? 일반적으로 우리가 비동기-논블로킹을 구현하는 방법은 메인스레드 내에서 추가적인 스레드를 생성해서 사용하는 것이다.

 

그 스레드에서 I/O 작업을 처리하도록 하는 것인데 이 경우 메인 스레드는 블로킹 없이 곧바로 CPU 자원을 반납할 수 있다. 그런데 몇가지 애매한 점 이 있다.

  1. 단순 논블로킹 작업이라면 상관없지만 어찌되었건 결과가 필요한 작업이라면 어찌되었던 추가로 생성한 스레드의 작업 완료를 기다려야 한다. 동기-논블로킹 작업이 되는 셈이고 메인스레드가 블로킹에 빠지는 것은 동일하다는 말이다.
  2. 어찌되었건 추가적인 스레드가 필요하다는 것인데 결국 블로킹에 빠지는 스레드가 바뀌었을 뿐 블로킹은 발생하게 되고 이에 따른 CPU 점유는 피할 수 없다.
  3. 추가로 생성하는 스레드도 무한정으로 늘릴 수는 없기 때문에 지정된 갯수를 풀로 관리해야 한다. 하지만 그 풀의 스레드도 다 사용하면 메인스레드는 또 다시 블로킹 상태가 된다.

그런데 사실 1번은 생각하는 관점을 바꿔야 한다. 1개의 I/O 작업만 있다면 의미가 없겠지만 그 이상의 I/O 작업이 포함되어 있다면 작업의 갯수가 많아질수록 처리시간은 획기적으로 줄어들 수 있다.

 

예를들어 3개의 I/O 작업이 있고 각각 소요시간이 1초, 2초, 3초라고 가정해보자. 이들을 동기로 처리한다면 총 처리시간은 1초+2초+3초=6초가 될 것이다. 하지만 동기-논블로킹으로 처리한다면 총 처리시간은 작업 중 가장 긴 소요시간인 3초가 된다.

 

2, 3번은 추가적인 스레드를 생성하지 않고 외부 메시지큐(카프카 등) 도입을 통해 메인스레드와 실제 작업자를 완전히 분리하면 해결될 수 있을 것만 같다. 이 경우 완전한 비동기-논블로킹 작업이 된다.

 

현 상황에서 가장 보편적으로 생각해볼 수 있는 것이 카프카다. 하지만 카프카는 메인스레드와 작업자가 완전히 분리된다.

 

요청에 대한 결과가 메인스레드의 종료시점과 무관하게 적용되어도 된다면 상관없겠지만 웹서비스는 결국 어떠한 결과를 response 로 제공해야 하고 그러기 위해서는 메인스레드와 작업자는 어떻게든 연결이 되어 있어야 한다.

 

1번에서 말했지만 엄밀히 따지면 우리에게 필요한 것은 동기-논블로킹 처리이다. 때문에 “요청에 대한 결과가 메인스레드의 종료시점과 무관하게 적용되어도 되는" 특수한 상황이 아니라면 카프카 같은 메시지큐는 고려 대상이 아니다.

 

이러한 동기-논블로킹 처리의 요구를 충족하기 위해 자바에서 사용되던 것은 ComplatableFuture 이다. 그렇다면 이것만으로도 충분할 거 같은데 리액티브 프로그래밍은 무엇이고 왜 필요한 걸까?

 

우선 리액티브 프로그래밍의 핵심 원칙은 다음과 같다.

  1. 반응성: 빠를 뿐 아니라 일정하고 예상할수 있는 반응 시간을 제공한다.
  2. 회복성: 장애가 발생해도 시스템은 반응해야 한다.
  3. 탄력성: 다양한 작업 부하가 발생하면 관련 컴포넌트에 할당된 자원 수를 늘리는 등 자동으로 대응이 되어야 한다.
  4. 메시지 주도: 회복성과 탄력성을 지원하려면 약한 결합, 고립, 참조 투명성 등을 지원할 수 있도록 데이터 발행자/구독자를 구분하고 비동기 메시지 기반으로 통신하도록 한다.

ComplatableFuture 는 특정 로직의 독립적 실행과 병렬성에만 목적을 두는 반면 리액티브 프로그래밍에서는 전체 프로세스를 대상으로 하고 독립적 실행과 병렬성은 핵심 원칙을 지키기 위한 방법 중 하나이고 그 외 비동기 처리에 필요한 여러 표준들이 포함되어 있다.

 

이로 인해 비동기 구현의 추상 수준을 높일 수 있으므로 저수준의 문제들을 직접 처리할 필요가 없어지면서 비즈니스 요구사항을 구현하는 데 더 집중할 수 있게된다.

 

또 다른 이유는 위에서 말했듯이 ComplatableFuture 는 get() 시점에 블로킹이 발생할 수 밖에 없기 때문에 메인스레드가 블로킹에서 완전히 자유로울 수 없는 동기-논블로킹 방식인 반면 리액티브 프로그래밍은 메인스레드가 블로킹에 빠지지 않는 비동기-논블로킹 환경을 제공한다.(Netty)

 

리액티브 프로그래밍은 특정 작업을 비동기로 처리하는 것이 아닌 전체적인 데이터가 흐를 파이프라인이 구성된 스트림이고 이 스트림은 비동기 데이터 처리의 표준으로 리액티브 스트림이라고 부른다.

 

넷플릭스, 레드햇, 트위터, 라이트밴드 및 기타 회사들이 참여한 리액티브 스트림 프로젝트에서는 리액티브 스트림이 제공해야 하는 최소 기능 집합을 인터페이스로 정의했지만 해당 인터페이스는 매우 단순해서 개발자가 직접 다루기보다는 프레임워크의 기초를 이루며 상호 운영성을 높이는데 사용된다.

 

자바의 Flow, Akka 스트림, 리액터, RxJava, Vert.x 등 많은 서드파티 라이브러리나 프레임워크가 리액티브 스트림 인터페이스를 구현하고 있다.

 

이 중 스프링 웹플럭스에서 사용되는 것이 리액터(Project Reactor) 이고 리액터를 사용하면 다음의 특성을 따르는 리액티브 프로그래밍을 할수 있게 된다.

 

  • 비동기-논블로킹 프로그래밍
  • 함수형 프로그래밍
  • 스레드를 신경 쓸 필요없는 동시성

 

https://velog.io/@deannn/Apache와-NginX-비교-차이점
https://ooeunz.tistory.com/149https://devsh.tistory.com/m/entry/스프링-웹플럭스와-코루틴-톺아보기
https://snoop-study.tistory.com/85
https://popcorntree.tistory.com/2
https://devsh.tistory.com/entry/리액티브-프로그래밍-기초-3-리액티브-스트림
https://dev-daddy.tistory.com/25
https://12bme.tistory.com/570
http://ruaa.me/why-functional-matters/
https://sjh836.tistory.com/182
https://perfectacle.github.io/2019/03/10/how-can-webflux-process-huge-requests-with-fewer-threads/
모던 자바 인 액션
728x90

'Base > 개념정리' 카테고리의 다른 글

Netty  (1) 2022.07.21
C10K  (0) 2022.06.14
서로 다른 사설 네트워크 서버끼리 통신하기  (0) 2022.04.27
ResponseTimeout, ReadTimeout, WriteTimeout  (0) 2022.04.19
URI, URL  (0) 2021.12.16