Reactive Programming ‐ Reactive System & Reactive Programming - thought-corner/Backend-PlayGround GitHub Wiki
Reactive Programming - Reactive System & Reactive Programming
MVC 기반 아키텍처 흐름과 리액티브 프로그래밍 탄생 배경
1. Blocking I/O와 스레드 풀 고갈
- 200개의 스레드가 모두 DB 조회(200ms)나 외부 API 호출(500ms) 결과를 기다리며 대기(Block) 상태에 빠지게 된다.
- 리소스 낭비 : 이 대기 시간 동안 스레드들은 CPU 연산을 전혀 수행하지 않으면서도, 단지 응답이 오기를 기다린다는 이유만으로 시스템 리소스(스레드)를 점유하고 있다.
- 스레드 풀 고갈(Thread Pool Exhaustion) : 기본 Tomcat 설정인 200개의 스레드가 모두 이런 I/O 대기 상태에 묶여버리면, 이후에 들어오는 201번째 요청은 처리할 여유 스레드가 없어 큐에서 대기하게 되고, 결국 타임아웃이나 Connection Refused 에러와 함께 요청이 유실된다.
2. 스레드를 늘리는 것의 물리적 한계
- 쓰레드라는 자원은 공짜가 아니라는 것을 명심해야 한다.
- 메모리 오버헤드 : 스레드는 생성될 때마다 독립적인 스택 메모리(일반적으로 1MB)를 할당받는다. 스레드가 기하급수적으로 늘어나면 애플리케이션의 메모리 점유율도 감당할 수 없이 커지게 된다.
- 컨텍스트 스위칭(Context Switching) 비용 : CPU 코어 수는 제한적인데 동작해야 할 스레드만 많아지면, CPU가 여러 스레드를 번갈아 실행하기 위해 현재 상태를 저장하고 다른 상태를 복원하는 작업에 과도한 연산 능력을 낭비하게 되어 오히려 전체 처리량(Throughput)이 저하된다.
3. 패러다임의 전환: 리액티브 프로그래밍과 Non-Blocking I/O
- Event Loop 기반 비동기 처리 : 요청이 들어오면 대규모 스레드 풀 대신, 소수의 스레드(보통 CPU 코어 수와 동일하거나 2배)로 동작하는 이벤트 루프가 이를 수신한다.
- 작업 위임과 즉시 반환 : 스레드가 DB나 외부 API에 데이터를 요청한 뒤, 결과를 그 자리에서 기다리지 않고 즉시 스레드 풀로 돌아가 대기 중인 다른 클라이언트의 요청을 처리한다.
- 이벤트 콜백 : DB나 API로부터 응답 데이터가 준비되어 도착하면 시스템에 이벤트를 발생시키고, 여유가 있는 스레드가 해당 결과값을 받아 나머지 비즈니스 로직을 마저 처리한 뒤 클라이언트에게 응답한다.
4. 진정한 리액티브 아키텍처를 위한 조건
- 리액티브 스택의 높은 동시성 처리 이점을 극대화하려면 웹 계층만 비동기로 동작해서는 안 되며, 데이터베이스 접근 계층까지 모두 Non-Blocking으로 구성해야한다.
- 웹 계층을 WebFlux로 구현했더라도 DB 연결에 기존의 동기식 JDBC를 사용한다면 스레드는 결국 DB 응답을 기다리며 블로킹된다.
- 따라서 다수의 외부 API를 호출하여 시세 데이터를 병합하는 크롤러나, 높은 트래픽의 금융 데이터를 다루는 아키텍처에서는 기존 패러다임을 깨고 R2DBC와 같은 비동기 논블로킹 DB 드라이버를 결합해야만 진정한 리액티브 시스템의 성능을 끌어낼 수 있다.
데이터 스트림 구조에서 OOM 방지를 위한 BackPressure 효과
- 트래픽이 폭주하더라도 시스템의 어느 한쪽이 OOM 등으로 무너지는 것을 막고 자신의 처리 능력(Capacity) 안에서 가장 안전하고 효율적으로 동작하도록 보장하는 방어 메커니즘이다.
- Consumer-Driven : 데이터를 만들어내는 쪽(Publisher)이 일방적으로 데이터를 밀어 넣는(Push) 것이 아니라, 데이터를 소비하는 쪽(Subscriber)이 철저하게 트래픽의 주도권을 쥐게 된다.
- Dynamic Adjustment : 실시간으로 데이터 수신량을 조절한다.
- Graceful Completion : 모든 데이터를 동적으로 보내고 지체 없이
onComplete()를 호출하여 자원 낭비 없이 안전하게 통신을 종료한다.
Spring MVC와 Spring WebFlux의 차이점과 주의사항 정리
1. 쓰레드 할당 및 필터 단계
- 클라이언트 요청 → 쓰레드 풀로부터 쓰레드를 할당
- 사용자의 HTTP 요청이 WAS에 도달하면 WAS가 관리하는 쓰레드 풀에서 유휴 쓰레드 하나를 꺼내 해당 요청에 할당한다.
- 서블릿 필터 : 할당된 쓰레드는 Spring 컨텍스트에 진입하기 전 톰캣과 같은 서블릿 컨테이너 레벨에서 공통 관심사를 처리하는 필터 영역을 거치게 된다.
2. Spring MVC 핵심 컨트롤러
- 디스패처 서블릿(Dispatcher Servlet) : 필터를 통과한 요청은 디스패처 서블릿으로 들어온다. 모든 요청을 중앙에서 받아 적절한 곳으로 위임하는 프론트 컨트롤러 패턴이다.
HandlerMapping + Controller: 해당 URI 요청을 처리할 수 있는 컨트롤러 메서드가 있는지를 조회 후 매핑된 Controller로 요청을 전달, 컨트롤러는 사용자가 보낸 파라미터를 검증 및 바인딩한 뒤 비즈니스 로직으로 넘긴다.
3. 비즈니스 로직 및 데이터 접근 단계
- Service 레이어에서 핵심 비즈니스 로직이 실행된다.
- 데이터를 조회하거나 저장하기 위해 Repository를 호출하여 데이터베이스와 통신한다.
- 핵심 포인트 : 전통적인 JDBC나 비동기를 지원하지 않는 드라이버를 사용할 경우, DB에 쿼리를 날리고 결과를 받아올 때까지 해당 쓰레드는 블로킹 상태가 된다. 이 구간이 시스템 전체의 처리량(Throughput)을 제약하는 주요 병목 구간이 된다.
4. 응답 및 스레드 반납
- DB 조회가 완료되면 Dispatcher Servlet으로 결과가 리턴된다.
- 쓰레드 반납 : 클라이언트에게 최종 HTTP 응답이 전송되면 해당 요청을 처리하기 위해 사용했던 쓰레드는 파괴되지 않고, 다시 WAS의 쓰레드 풀에 반납되어 다음 요청을 대기한다.
1. 이벤트 루프
- 이벤트 루프는 무한히 회전하며 큐(Queue)에 들어오는 이벤트(클라이언트 요청, DB 응답 등)를 확인하고 처리하는 싱글 쓰레드이다.
2. 요청 1 유입 및 작업 위임
- 요청 1이 들어오는데 만약 이 요청이 CPU만 쓰는 단순 계산이라면 이벤트 루프가 바로 처리해서 응답한다.
- 하지만 DB 조회, 외부 API 호출, 파일 읽기 등 오래 걸리는 I/O 작업이라면 이벤트 루프는 자기가 직접 기다리지 않고 운영체제나 백그라운드 쓰레드 풀에 해당 작업을 처리해달라고 요청을 등록한다.
3. 막힘없는 동시 처리
- 요청 1의 I/O를 던져버린 이벤트 루프는 블로킹되지 않는다.
- 즉시 다음으로 들어온 요청 2, 요청 3을 받아서 똑같이 처리하거나 위임한다. 하나의 쓰레드로 수천, 수만 개의 동시 커넥션을 맺을 수 있는 이유가 바로 Non-Blocking 기반이기 때문이다.
4. 완료 이벤트
- 백그라운드에서 요청 1의 I/O 작업이 끝나고 이벤트 루프 측에 작업이 다 되었다는 완료 이벤트를 보낸다.
5. 응답 처리
- 계속 들고 있던 이벤트 루프가 이 완료 이벤트를 발견하고 비로소 조회된 데이터를 가공해 요청 1을 보냈던 클라이언트에게 최종 응답을 내려준다.