
Spring WebMvc는 Servlet API 기반의 동기(Synchronous), 블로킹(Blocking) I/O 모델을 사용함. Spring 프레임워크의 시작과 함께한, 가장 전통적이고 널리 사용되는 스택임.
요청 당 스레드 (Thread-per-Request)💡 비유: 전담 바리스타
WebMvc는손님 한 명을 끝까지 책임지는 바리스타와 같음.
바리스타(스레드)는 손님(요청)의 주문을 받고, 커피 원두를 갈고, 에스프레소를 내리는 모든 과정이 끝날 때까지 그 손님 옆에서 기다림. 다른 손님이 와도, 지금 손님의 커피가 완성되기 전까지는 응대할 수 없음. 손님이 많아지면 바리스타를 더 많이 고용해야 함.
Spring 5부터 등장한 WebFlux는 비동기(Asynchronous), 논블로킹(Non-Blocking) I/O 모델을 기반으로 하는 반응형(Reactive) 웹 스택임. 적은 리소스로 높은 처리량을 목표로 함.
이벤트 루프 (Event Loop)💡 비유: 효율적인 멀티태스킹 바리스타
WebFlux는진동벨을 활용하는 바리스타와 같음.
바리스타(스레드)는 손님(요청)의 주문을 받고, 진동벨(Mono/Flux)을 나눠준 뒤 바로 다음 손님의 주문을 받음. 커피 제작은 머신에 맡기고, 자신은 계속해서 주문을 처리함. 커피가 완성되면 진동벨이 울리고(이벤트), 손님은 커피를 받아감. 한 명의 바리스타가 동시에 여러 손님을 효율적으로 관리할 수 있음.
간단한 사용자 조회 API를 통해 두 스택의 코드 스타일 차이를 확인해보자.
// Lombok 어노테이션을 사용해 코드 간소화
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String id;
private String name;
}
반환 타입은 User와 같은 일반 객체. 코드가 위에서 아래로 순차적으로 실행됨.
userService.findById는 결과를 받을 때까지 현재 스레드를 블로킹함.
@RestController
@RequestMapping("/mvc/users")
@RequiredArgsConstructor // final 필드에 대한 생성자 자동 주입
public class UserMvcController {
private final BlockingUserService userService;
@GetMapping("/{id}")
public User getUserById(@PathVariable String id) {
// 이 메서드는 User 객체를 반환할 때까지 스레드를 점유함.
return userService.findById(id);
}
}
반환 타입은 Mono<User>(0-1개 데이터) 또는 Flux<User>(0-N개 데이터) 같은 Reactive Type. 데이터의 흐름을 정의하는 파이프라인을 구축함.
@RestController
@RequestMapping("/webflux/users")
@RequiredArgsConstructor
public class UserWebFluxController {
private final ReactiveUserService userService;
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable String id) {
// 이 메서드는 즉시 Mono<User> 객체를 반환하고 스레드를 놓아줌.
// 실제 데이터는 비동기적으로 처리되어 파이프라인을 따라 흐름.
return userService.findById(id)
.doOnSuccess(user -> log.info("User found: {}", user.getName())); // 작업 완료 시 로그 출력 (논블로킹)
}
}
WebFlux에서는 .map(), .filter(), .doOnSuccess() 같은 연산자를 체이닝하여 데이터 처리 과정을 선언적으로 정의함. 스레드는 이 선언을 등록하고 즉시 다른 일을 하러 간다는 점이 핵심.
어떤 스택을 선택할지 고민될 때, 스스로에게 다음 질문을 던져보자.
BlockHound 같은 라이브러리로 블로킹 호출을 탐지할 수 있음.