본문으로 건너뛰기

14.1 리액티브 선언문과 WebFlux vs MVC 스레드 모델 차이

과거 백엔드 생태계를 장악하던 Spring MVC 는, 1만 명의 유저가 똑같은 상품 페이지를 조회하면 무식하게 "10,000명의 톰캣 일꾼 스레드(Thread)"를 병렬로 띄워 대응했습니다(Thread-per-Request 모델). 코어 수는 8개뿐인데 스레드 컨텍스트 스위칭 연산만 하다가 CPU가 폭주하고 뻗어버리는 문제가 있었습니다.

이를 극복하기 위해 Node.js의 철학을 흡수한 스프링 진영의 비동기 논블로킹 엔진, Spring WebFlux (웹플럭스) 가 등장했습니다.


🛑 1. 레거시 MVC의 한계 (동기적 블로킹 구조)

MVC의 흐름에서는 스레드가 외부 API를 호출하거나 DB 쿼리를 던지고 나면, 응답이 돌아올 때까지 다음 일을 하지 못하고(하품하며) 그 자리에 멈춰 서 있는 블로킹(Blocking)과 동기(Synchronous) 현상이 벌어집니다.

// Spring MVC의 전형적인 블로킹 구조
@GetMapping("/payment")
public String pay() {
// 1번: 외부 은행 서버(느림)에 결제 검증 요청을 날린다.
String result = restTemplate.getForObject("http://slow-bank.com/chk", String.class);
// 🚫 [병목 발생]: 응답(result)이 올 때까지 현재 톰캣 스레드(나사 조이는 일꾼)는 멈춰서 노가리를 깐다.
// 이런 일꾼이 200명 꽉 차면 201번째 유저부터는 접속 지연 폭탄을 맞음!

// 2번: 완료되면 결과 저장
repository.save(new Log(result));
return "SUCCESS"; // 끝
}

⚡ 2. 리액티브 논블로킹의 기적 (Event Loop)

웹플럭스는 코어 수만큼의 극소수 스레드(통상 4~8개) 만을 사용하여 이 수만 건의 트래픽을 처리합니다. 어떻게 일꾼 4명이 공장 라인 만 개를 감당할까요?

논블로킹(Non-Blocking) 이벤트 루프 아키텍처 덕분입니다. "제가 외부 은행에 결제 요청 편지를 보내긴 할 건데요, 저 안 기다리고 다음 손님 주문 계속 받을게요. 은행에서 응답 도착하면 옆방 콜백 호출기(Event Loop)가 삐삐 쳐서 알려주세요."

가장 핵심이 되는 두 가지 반응형 객체가 바로 Mono(결과 01개 반환)와 Flux(결과 0N개 나열 반환)입니다.

// Spring WebFlux의 완벽한 논블로킹 비동기 구조
@GetMapping("/payment-flux")
public Mono<String> payReactive() {

return webClient.get()
.uri("http://slow-bank.com/chk")
.retrieve()
.bodyToMono(String.class) // 여기까지 결제 요청 날림 편지 예약
.flatMap(result -> {
// 이 블록은 1초 뒤 은행에서 결과가 도착하면 다른 여유분 스레드가 콜백으로 이어서 작업함
return r2dbcRepository.save(new Log(result))
.map(saved -> "SUCCESS"); // 저장 완료되면 맵핑
});
// 🚫 [기적 발생]: 이 메서드 자체는 눈 깜짝할 새인 0.001초 만에 논블로킹으로 끝남(리턴 Mono)
// 톰캣 일꾼이 응답 안 기다리고 즉각 퇴근 후 바로 뒷 손님 주문을 받으러 감!
}

🌊 3. 배압 (Backpressure): 리액티브의 핵심 철학

비동기 시스템에서 "주는 쪽"의 속도가 "받는 쪽"의 속도를 아득히 넘어서 메모리가 터지는 현상이 벌어집니다. 예를 들어 1초에 1억 건의 주식 차트를 쏟아내는 Flux의 폭포수를, 메모리가 2GB뿐인 가난한 안드로이드 앱 클라이언트가 온몸으로 다 받아내려면 RAM 메모리가 Out of Memory로 터져 나갑니다.

이 현상을 막기 위해 받는 쪽이 "야 나 지금 소화 안 되니까 딱 10개씩만 천천히 좀 보내줘(Request N)" 라고 역으로 피드백을 주는 브레이크 메커니즘이 바로 배압(Backpressure) 입니다.

// 배압을 조작하는 Flux 흐름
Flux.range(1, 100000)
.doOnNext(i -> System.out.println("발행 중: " + i))
.limitRate(10) // 💡 [핵심 배압 튜닝] 클라이언트가 10개씩만 뜯어가게 청크 단위 조절 방파제 설치
.subscribe(
data -> System.out.println("소비 중(천천히..): " + data),
err -> System.err.println("오류!"),
() -> System.out.println("10만 개 처리 퍼펙트 완료!")
);

🎯 4. 고수 팁 (Pro Tips)

💡 파멸의 조합: WebFlux 엔진에 전통 JPA 섞기

정말 많은 초보들이 "스프링 웹플럭스가 속도 킹이래!" 라면서 웹플럭스를 세팅해 놓고, DB 접속 툴로는 구형 spring-boot-starter-data-jpaJDBC를 가져다 씁니다.

결과: 서버가 MVC 시절보다 1000배 느려지거나 다운됩니다.

웹플럭스의 코어 일꾼 스레드는 4명뿐입니다. 그런데 전통적인 JPA와 JDBC 통신 드라이버는 태생이 "완전한 Blocking(동기)" 스레드입니다. 단 4번의 DB SELECT 통신이 몰릴 때 4명의 일꾼 모두 하품하며 응답을 대기하느라(Blocking), 공장 전체 시스템이 완전 멈추는 데디락 파국의 대참사가 터집니다.

해결책: 웹플럭스를 쓴다면 데이터베이스 접속 툴 역시 반드시 비동기 논블로킹을 완벽히 지원하는 R2DBC (Reactive Relational Database Connectivity) 나 리액티브 몽고디비(spring-boot-starter-data-mongodb-reactive)를 깔맞춤 세트로 사용해야만 100% 성능 마법이 발휘됩니다.