리액티브 프로그래밍
- 데이터 또는 이벤트의 변경이 발생하면 이에 반응해 처리하는 프로그래밍 기법
- 비동기 프로그래밍을 처리하는 새로운 접근 방식
- 데이터의 통지, 완료, 에러에 대한 처리를 Observer Pattern에서 영감을 받아 설계되었고, 데이터의 손쉬운 비동기 처리를 위해 함수형 언어의 접근 방식을 사용하는 특징이 있다.
기존 비동기 프로그래밍의 문제점과 리액티브 프로그래밍의 장점
- 기존의 비동기 프로그래밍은 콜백 (Callback) 기반의 비동기 처리 방식을 사용했지만, 콜백이 많아져서 발생하는 Callback Hell로 인해 코드의 복잡도가 늘어나고 가독성과 유지보수성이 떨어지는 문제가 발생한다.
→ 리액티브 프로그래밍을 적용하면 Callback Hell 없이 비동기 기반의 non-blocking과 Event-Driven 애플리케이션 구현에 유리하다.
백프레셔 (Back Pressure) 개념
또한, 백프레셔라는 특징이 있는데, 데이터를 소비하는 측에서 처리 가능한 만큼의 양을 데이터 제공자 측에 역으로 요청하는 기능이다
→ 데이터를 처리하는 측에서 데이터를 전달받는 개수를 역으로 요청하여, 데이터를 통지하는 주체의 통지 속도를 제어하고, 자신이 처리 가능한 만큼의 데이터만 받아서 처리 가능하다.
제공자 측에서 너무 빠르게 데이터를 제공하면 소비자 측에서 빠르게 처리하지 못하고, 데이터가 점점 쌓이면 시스템에 장애 요소가 될 수 있다 (빠른 프로듀서, 느린 컨슈머 이슈).
주로 제공자와 소비자가 다른 스레드에서 비동기로 처리되는 경우 사용됨
옵저버 패턴 (Observer Pattern)
옵저버 패턴의 개념
관찰 대상이 되는 객체가 변경되면 대상 객체를 관찰하고 있는 옵저버에게 변경사항을 통지하는 디자인 패턴이다. 이벤트를 발생시키는 역할 (Subject)과 이벤트를 수신하는 역할 (Observer)이 있으며, 이벤트 기반으로 처리된다.
객체 간의 상호작용을 쉽게 하고 효과적으로 데이터를 전달할 수 있다.
옵저버 패턴의 문제점
- 데이터 처리 과정에서 에러 처리에 대한 문제 (Observer가 준비되지 않은 상태에서 Subject에서 데이터를 전송한 경우)
→ Reactive Streams에서는 onError라는 메소드를 통해 에러에 대한 데이터 전달 가능 - 데이터를 전송할 경우 완료의 개념이 존재하지 않음 → onNext, onCompletion을 통해 완료를 통보
- 빠른 프로듀서와 느린 컨슈머 이슈 → Backpressure를 통해 문제 해결
리액티브 프로그래밍에서는 옵저버 패턴의 서브젝트와 옵저버의 개념과 유사한 발행자(Publisher)와 구독자(Subscriber)를 사용해 데이터를 통지하고 처리한다.
이터레이터 패턴 (Iterator Pattern)
이터레이터 패턴의 개념
데이터의 집합에서 원소라고 불리는 데이터를 순차적으로 꺼내기 위해 만들어진 디자인 패턴
다른 컬렉션을 사용해도 동일한 인터페이스를 사용해 데이터를 꺼내올 수 있기 때문에 컬렉션이 변경되더라도 사용하는 쪽에서는 변경사항이 발생하지 않는다.
이터레이터 패턴과 리액티브 프로그래밍의 차이점
리액티브 프로그래밍과 이터레이터 패턴은 데이터를 제공한다는 관점에서 유사하지만 다음과 같은 차이점이 있다
- 이터레이터 패턴: 내부에 데이터를 저장하고, 이터레이터를 사용하여 데이터를 순차적으로 당겨오는 Pull 기반 (Pull-Based) 방식이다.
- 리액티브 프로그래밍: 옵저버 패턴처럼 제공자가 소비하는 측에 데이터를 통지하는 Push 기반 (Push-Based) 방식이다.
비동기 처리 방법 (Asynchronous Processing Methods)
다양한 비동기 처리 방법
비동기 처리 방법에는 대표적으로 스레드(Thread), 콜백(Callback), 프로미스(Promise), 퓨처(Future), 코루틴(Coroutine) 등이 있으며, 각각의 방법들은 장단점이 존재하며 언어와 라이브러리에 따라 지원되지 않는 경우도 있다.
리액티브 프로그래밍에서 비동기 처리를 적용하면 콜백 헬에서 벗어날 수 있고, 퓨처를 사용했을 때 성능 하락을 유발하는 블로킹 문제를 논블로킹 형태로 쉽게 변경할 수 있다.
스레드 (Thread)
서버 프로그래밍에서 가장 기본이 되는 비동기 처리 방식이다.
- 하나의 프로세스에는 최소한 하나 이상의 스레드가 존재하고, 프로세스 내의 스레드들은 동일한 메모리를 공유한다.
- 일반적으로 하나의 프로세스에 스레드가 1개인 경우 싱글 스레드라고 하고, 하나 이상 존재하는 경우 멀티 스레드라고 한다.
- 멀티 스레드는 스케줄링 알고리즘에 의해 컨텍스트 스위칭이 일어나며, 다른 스레드로 전환된다.
하지만, 스레드가 무한정 많아지면 메모리 사용량이 증가하여 OutOfMemoryError가 발생할 수 있으며, 스레드를 생성하면서 발생하는 대기 시간 때문에 빠른 응답을 주지 못하는 경우가 발생한다. 이러한 문제를 극복하기 위해 스레드 풀 (Thread Pool) 을 사용한다. 스레드 풀은 애플리케이션 내에서 사용할 총 스레드를 제한하고, 이미 생성된 스레드를 재사용한다.
퓨처 (Future)
퓨처는 비동기 작업에 대한 결과를 얻고 싶은 경우 사용되는 인터페이스이다.
- 스레드를 직접 사용하는 것보다 직관적이기 때문에 이해하기 쉽다.
- 모든 작업이 끝나고 작업에 대한 결과를 얻어 별도의 처리를 할 수 있다.
함수형 프로그래밍과 리액티브 프로그래밍
함수형 프로그래밍의 필요성
콜백이나 옵저버 패턴을 넘어서 RxJava 기반의 리액티브 프로그래밍이 되려면 함수형 프로그래밍이 필요하다.
- 콜백이나 옵저버 패턴은 옵저버가 1개이거나 단일 스레드 환경에서는 문제가 없지만, 멀티 스레드 환경에서는 데드락과 동기화 문제를 주의해야 한다.
- 함수형 프로그래밍은 부수 효과(side effect)가 없다.
- 콜백이나 옵저버 패턴이 스레드에 안전하지 않은 이유는 여러 스레드가 같은 자원에 접근하면서 경쟁 조건(race condition)에 빠질 경우 예측할 수 없는 결과가 나오기 때문이다.
- 멀티 스레드 환경에서 안전성을 확보하려면 부수 효과가 없는 순수 함수(pure function) 를 활용해야 한다.
- 따라서 자바에서 리액티브 프로그래밍을 하려면 함수형 프로그래밍의 지원이 필요하다.
'Spring WebFlux' 카테고리의 다른 글
[Spring WebFlux] Reactive Streams (0) | 2025.02.26 |
---|---|
[Spring WebFlux] Project Reactor (0) | 2025.02.15 |
[Spring WebFlux] Blocking vs Non-Blocking & Sync vs Async (0) | 2025.02.15 |