최근 대규모 트래픽 처리, 외부 API 연동, MSA 환경 등에서 효율적인 서버 개발이 점점 중요해지면서, WebFlux와 같은 논블로킹 리액티브 웹 프레임워크를 이해하고 활용하는 것이 필요해졌습니다.
그래서 이 글에서는 Spring MVC와 WebFlux의 차이, 언제 무엇을 선택해야 하는지, 장단점을 작성해보려고 합니다.
먼저 차이를 알아보기 전에 동기/비동기 블로킹/논블로킹에 대해 간단한 개념 정리하고 가겠습니다.
Sync Blocking (동기 + 블로킹)
- "커피 주문하고 나올 때까지 카운터 앞에서 멍하니 기다리기"
- 요청을 보내고 결과가 올 때까지 제어권을 뺏긴 채 아무것도 하지 않고 기다림.
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("https://example.com/get", String.class);
System.out.println(result);
Async Blocking (비동기 + 블로킹)
- "진동벨은 받았지만(비동기), 진동벨만 쳐다보며 아무것도 안하기"
- 비동기 작업을 요청했지만, 결국 그 결과가 나올 때까지 제어권을 얻지 못하고 대기하는 상황.
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() ->
restTemplate.getForObject("https://example.com/get", String.class)
);
System.out.println(future.get());
Sync Non-Blocking (동기 + 논블로킹)
- "커피 시키고 딴짓하다가, '다 됐어요?' 하고 물어보기"
- 요청 후 즉시 제어권을 돌려받아 다른 일을 할 수 있다.(Non-Blocking) 하지만 작업이 완료되었는지 확인하기 위해 계속해서 상태를 체크(Polling)해야 한다.
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<String> future = executor.submit(() ->
restTemplate.getForObject("https://example.com/get", String.class)
);
System.out.println(future.get());
Async Non-Blocking (비동기 + 논블로킹)
- "커피 시키고 자리에 앉아 일하다가, 진동벨 울리면 가지로 가기"
- 요청 후 즉시 제어권을 받아 다른 일을 처리한다.(Non-Blocking) 작업이 끝나면 '완료되었다'는 신호(Callback/Event)를 받아 결과를 처리한다.(Async)
WebClient webClient = WebClient.create();
Mono<String> result = webClient.get()
.uri("https://example.com/get")
.retrieve()
.bodyToMono(String.class);
result.subscribe(System.out::println);
1. Spring MVC란?
Spring MVC는 전통적인 Servlet 기반 웹 프레임워크.
Tomcat 같은 WAS 위에서 동작하며, 동기·블로킹 모델을 사용.
특징
Servlet API 기반
- 요청 1개 = 쓰레드 1개(Thread-per-request)
- FreeMarker, Thymeleaf, JSP 같은 템플릿 엔진과도 친함.
- 대부분의 라이브러리(JPA, MyBatis 등)와 폭넓은 호환성.
동작 흐름
- 요청 → 컨트롤러 → 서비스 → DB 호출 → 응답
- 모든 단계가 기본적으로 동기/블로킹.
- 즉, DB를 200ms 동안 조회하면 해당 요청 스레드는 200ms 동안 멈춤.
장점
- 익숙하고 배우기 쉬움
- 디버깅 쉬움
- 기존 레거시와 호환성 높음
- 웹 서비스 대부분에서 충분한 성능
단점
- 블로킹 I/O가 많은 경우 성능이 부족할 수 있음
- 많은 요청이 동시에 들어오면 스레드 부족 문제 발생
- 대표적으로 CPU나 I/O가 많은 대규모 트래픽 상황에서는 병목이 생긴다.
2. Spring WebFlux란?
Spring WebFlux는 논블로킹·리액티브 프로그래밍 기반의 웹 프레임워크.
핵심 기반은 Reactor (Mono, Flux)이며, Netty 같은 논블로킹 서버 엔진을 사용할 수 있음.
특징
- Non-blocking & Asynchronous
- Reactive Streams 기반
- Mono(단일), Flux(다중)를 통해 데이터 전달
- 소수의 스레드로도 높은 처리량(throughput)을 낼 수 있음
동작 구조
- 이벤트 루프 기반으로 동작하며, I/O 작업이 생기면 기다리지 않고 다른 요청을 처리한다.
장점
- 높은 동시성 처리에 유리
- 외부 API 호출이 많은 MSA 환경에서 강력함
- WebSocket, SSE 같은 스트리밍 환경과 궁합이 좋음
단점
- 리액티브 프로그래밍에 대한 학습 곡선
- 디버깅과 트레이싱이 MVC보다 어려움
- 모든 라이브러리가 리액티브를 지원하는 것이 아님
아래 코드는 순차 처리와 병렬 처리에 대한 비교이고 동시에 여러 요청을 보낼 때 차이가 명확해짐.
MVC는 스레드 풀이 고갈될 수 있지만, WebFlux는 이벤트 루프로 효율적으로 처리함.
=== 비교 완료: MVC 2089ms vs WebFlux 791ms ===
@GetMapping("/sequential")
public Mono<Map<String, Object>> compareSequentialProcessing(@RequestParam List<Long> ids) {
log.info("=== 순차 처리 vs 병렬 처리 비교 시작 ===");
// MVC 방식 (블로킹, 순차)
long mvcStart = System.currentTimeMillis();
List<Post> mvcPosts = mvcService.getMultiplePostsSequential(ids);
long mvcDuration = System.currentTimeMillis() - mvcStart;
log.info("MVC 처리 완료: {}ms", mvcDuration);
// WebFlux 방식 (논블로킹, 병렬)
return Mono.fromCallable(() -> System.currentTimeMillis())
.flatMap(webFluxStart -> webFluxService.getMultiplePostsParallel(ids)
.map(webFluxPosts -> {
long webFluxDuration = System.currentTimeMillis() - webFluxStart;
log.info("WebFlux 처리 완료: {}ms", webFluxDuration);
Map<String, Object> result = new HashMap<>();
result.put("mvc", Map.of(
"duration_ms", mvcDuration,
"count", mvcPosts.size(),
"type", "순차 처리 (블로킹)"
));
result.put("webflux", Map.of(
"duration_ms", webFluxDuration,
"count", webFluxPosts.size(),
"type", "병렬 처리 (논블로킹)"
));
result.put("performance_gain", String.format(
"WebFlux가 약 %.1f배 빠름",
(double) mvcDuration / webFluxDuration
));
log.info("=== 비교 완료: MVC {}ms vs WebFlux {}ms ===",
mvcDuration, webFluxDuration);
return result;
})
);
}
public List<Post> getMultiplePostsSequential(List<Long> ids) {
String threadName = Thread.currentThread().getName();
log.info("[MVC] 여러 게시물 순차 조회 시작: {} - Thread: {}", ids, threadName);
long startTime = System.currentTimeMillis();
List<Post> posts = ids.stream()
.map(this::getPost)
.collect(Collectors.toList());
long duration = System.currentTimeMillis() - startTime;
log.info("[MVC] 여러 게시물 순차 조회 완료 - 총 소요시간: {}ms, Thread: {}",
duration, threadName);
return posts;
}
public Mono<List<Post>> getMultiplePostsParallel(List<Long> ids) {
String threadName = Thread.currentThread().getName();
log.info("[WebFlux] 여러 게시물 병렬 조회 시작: {} - Thread: {}", ids, threadName);
long startTime = System.currentTimeMillis();
return Flux.fromIterable(ids)
.flatMap(this::getPost) // 모든 요청을 병렬로 실행
.collectList()
.doOnSuccess(posts -> {
long duration = System.currentTimeMillis() - startTime;
String currentThread = Thread.currentThread().getName();
log.info("[WebFlux] 여러 게시물 병렬 조회 완료 - 총 소요시간: {}ms, Thread: {}",
duration, currentThread);
});
}
MVC는 단순/안정적인 블로킹 웹 프레임워크이고 WebFlux는 고성능/논블로킹 리액티브 웹 프레임워크 이므로 둘 중 무엇이 더 좋다는 개념은 없고, 프로젝트 특성에 따라 선택하는것이 정답인거 같다.
'n년차 개발자' 카테고리의 다른 글
| 개인화 추천 API 성능 개선 (0) | 2025.11.27 |
|---|---|
| 서버 자원 수집 및 시각화 (0) | 2025.04.28 |
| Spring Boot Logstash 연동 설정 및 작동 방식 (1) | 2025.04.16 |
| 모니터링 시스템 구축 (0) | 2025.04.07 |
| AWS SDK 버전 충돌 (0) | 2025.04.05 |