JMeter로 1만 명의 부하 테스트를 때리다 보면, 평범한 Spring MVC(톰캣 기반) 서버가 어느 순간 맥없이 무너지는 것을 보게 됩니다. 이유가 뭘까요? 기존 방식은 '1 요청 = 1 쓰레드(Thread)'라는 치명적인 룰을 가집니다. 유저가 API를 호출하면 쓰레드 하나가 배정되고, 그 쓰레드는 데이터베이스에서 쿼리 결과를 가져오는 2초 동안 '대기(Blocking)' 상태에 빠집니다. 아무 일도 안 하면서 쓰레드를 꽉 쥐고 있는 거죠. 이렇게 200명의 유저가 동시에 접속해 쓰레드 200개가 전부 DB 응답을 기다리며 멈춰버리면, 201번째 유저부터는 하얀 화면만 보며 에러를 뱉게 됩니다(Thread Pool 고갈). "아니, DB 기다리는 동안 쓰레드가 놀지 말고, 돌아가서 다른 유저 요청을 받아오면 안 돼?" 이 상식적이고도 천재적인 발상에서 탄생한 차세대 기술이 바로 Spring WebFlux(웹플럭스)와 리액티브 프로그래밍(Reactive Programming)입니다.
이번 포스팅에서는 "수십 개의 쓰레드만으로 수만 명의 트래픽을 거뜬히 처리하는 마법!" WebFlux의 논블로킹 아키텍처와 패러다임 전환에 대하여 다뤄보겠습니다.

🧱 1. 블로킹(Blocking) vs 논블로킹(Non-Blocking)의 차이
스타벅스 커피 주문으로 비유해 봅시다.

- 기존 Spring MVC (블로킹): 알바생(쓰레드)이 손님(요청)에게 주문을 받습니다. 바리스타(DB)에게 "아아 1잔"을 외칩니다. 그리고 바리스타가 커피를 다 만들어서 줄 때까지 알바생은 계산대 앞에서 가만히 멍 때리고 서 있습니다. 뒤에 줄 선 100명의 손님은 분노합니다.
- Spring WebFlux (논블로킹): 알바생이 주문을 받자마자 바리스타에게 넘기고, 진동벨(Callback)을 손님에게 쥐여준 뒤 곧바로 뒤에 있는 다음 손님의 주문을 연달아 받습니다. 바리스타가 커피를 다 만들고 "띠동~" 호출하면, 그때서야 커피를 픽업해 전달합니다. 알바생 1명으로 수백 명의 손님을 커버할 수 있습니다!
WebFlux는 무거운 톰캣(Tomcat) 대신 Netty(네티)라는 비동기 전용 이벤트 루프(Event Loop) 웹 서버를 사용하여 이 기적을 구현합니다.
🔄 2. 리액티브 타입의 등장: Mono와 Flux
논블로킹을 구현하기 위해 자바 코딩 패러다임이 통째로 바뀝니다. 더 이상 User나 List<User>를 직접 리턴하지 않고, 미래에 데이터가 도착할 것이라는 '약속(Publisher)' 상자로 감싸서 던집니다.
// 과거 MVC 방식 (데이터가 나올 때까지 2초간 멈춤 - Blocking)
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id);
}
// 🌟 차세대 WebFlux 방식 (멈추지 않고 즉시 리턴함 - Non-Blocking)
@GetMapping("/users/{id}")
public Mono<User> getUserReactive(@PathVariable Long id) {
// 0~1개의 데이터를 담는 약속 상자는 Mono, 0~N개의 리스트를 담는 상자는 Flux라고 부릅니다.
// DB 조회 명령만 던져놓고, 데이터가 준비되면 알아서 유저에게 쏴주라고 약속 상자만 툭 던지고 끝냅니다!
return userReactiveRepository.findById(id);
}
💀 3. WebFlux 도입의 치명적인 함정 (R2DBC)
"와! 당장 내 프로젝트 WebFlux로 다 바꿔야지!"라고 생각하셨다면 잠시 멈추셔야 합니다. 엄청난 함정이 있습니다. 컨트롤러와 서비스를 아무리 WebFlux로 비동기로 짰더라도, 맨 마지막에 데이터베이스에 붙는 JPA(Hibernate)나 JDBC가 동기식(Blocking) 기술이라면 어떻게 될까요? 알바생이 논블로킹으로 주문을 백날 빨리 받아도, 바리스타(JPA)가 커피 머신 앞을 꽉 막고 있다면 결국 똑같이 멈춰버립니다.
따라서 WebFlux의 성능을 100% 뽑아내려면 JPA를 갖다 버리고, 비동기 논블로킹 전용 DB 드라이버인 R2DBC (Reactive Relational Database Connectivity)나 Spring Data Reactive MongoDB 등을 사용해야 합니다. 문제는 R2DBC가 JPA만큼 기능(연관관계, 지연로딩 등)이 풍부하지 않고 학습 곡선이 에베레스트산 급이라는 것입니다.
실무 결론: 일반적인 쇼핑몰이나 게시판(CRUD 위주)은 그냥 Spring MVC로 만들고 서버(EC2) 대수를 늘리는 것이 정신 건강에 이롭습니다. 하지만 채팅 서버, 주식 실시간 호가창, 또는 수많은 마이크로서비스(API)를 중간에서 취합해 주는 API Gateway 같은 'I/O 대기시간이 극도로 긴 특수 환경'에서는 WebFlux가 서버 비용을 10분의 1로 줄여주는 구원자가 됩니다.
🎯 4. 마무리 및 다음 단계
지금까지 쓰레드(Thread) 고갈의 한계를 부수고, 단 몇 개의 쓰레드만으로 엄청난 동시 접속자를 감당해 내는 현대 백엔드의 최전선 패러다임, Spring WebFlux와 Mono/Flux의 리액티브(Reactive) 아키텍처에 대해 다루어 보았습니다. 이제 여러분은 아키텍처를 결정할 때 "이 서비스에 정말 논블로킹이 필요한가?"를 판단할 수 있는 통찰력을 얻었습니다.
자, 드디어 [Spring Boot 실무 완벽 가이드] 시리즈의 대미를 장식할 마지막 포스팅입니다. 최근 AWS Lambda 같은 서버리스(Serverless) 환경이 뜨면서 자바(Java) 진영에 거대한 위기가 닥쳤습니다. 자바는 무거운 JVM(가상머신)을 띄우느라 서버 부팅에만 5~10초가 걸리는데, Go나 Node.js는 0.1초 만에 켜지기 때문입니다. "자바는 뚱뚱하고 느려서 서버리스 시대엔 끝났다"는 조롱을 잠재우기 위해, "JVM을 통째로 갈아버리고 자바 코드를 C언어 같은 순수 기계어(Native)로 바꿔치기해서 0.05초 만에 서버를 띄워버리는" 초특급 혁명 기술이 스프링 부트 3.0부터 전면 도입되었습니다. 시리즈의 마지막 30단계 포스팅에서는 "자바의 한계를 찢다! 서버 부팅 0.05초, 메모리 사용량 1/5! 차세대 백엔드의 궁극기" GraalVM Native Image 아키텍처와 Spring Boot 3 AOT 컴파일의 충격적인 실무 가이드에 대해 아주 뼈 때리게 파헤쳐 보겠습니다!
'Framework > Spring Boot' 카테고리의 다른 글
| [Spring Boot] WebSocket 실시간 채팅: 양방향 통신의 서막 (0) | 2026.08.05 |
|---|---|
| [Spring Boot] GraalVM Native Image: 자바의 한계를 찢고 0.05초 만에 부팅하라 (0) | 2026.07.27 |
| [Spring Boot] JMeter 부하 테스트: 트래픽 폭탄을 쏘고 병목을 부숴라 (0) | 2026.07.27 |
| [Spring Boot] 힙 덤프(Heap Dump) 분석: 메모리 누수(OOM)의 범인을 체포하라 (0) | 2026.07.27 |
| [Spring Boot] Docker 컨테이너화: "내 로컬에선 되는데 서버에선 안돼요"를 끝내다 (0) | 2026.07.27 |