본문 바로가기
Spring 7기 프로젝트/모임 플렛폼 프로젝트

WebFlux와 Spring MVC: 실무 관점에서의 차이점 이해하기

by JuNo_12 2025. 7. 25.

들어가며

우리 프로젝트에서는 도메인 간 통신을 위해 WebClient를 사용하고 있고, 이를 위해 WebFlux 의존성을 추가했습니다. 하지만 실제로는 WebFlux의 핵심인 반응형 프로그래밍의 극히 일부분만 사용하고 있는 상황입니다.

이 글에서는 Reactor의 복잡한 개념보다는 실무 관점에서 톰캣과 네티의 차이점, 그리고 언제 WebFlux를 고려해야 하는지에 대해 간단히 정리해보겠습니다.


현재 우리의 WebFlux 사용 현황

실제 사용 코드

@Component
@RequiredArgsConstructor
public class UserClient {
    
    private final WebClient webClient;
    
    // 우리가 실제로 사용하는 방식
    public UserClientResponseDto getUser(Long userId) {
        try {
            ApiResponse<UserClientResponseDto> response = webClient
                .get()
                .uri("/api/v2/users/{userId}", userId)
                .retrieve()
                .bodyToMono(new ParameterizedTypeReference<ApiResponse<UserClientResponseDto>>() {})
                .timeout(Duration.ofSeconds(5))
                .block();  // ← 여기서 동기적으로 변환
                
            return response != null && response.isSuccess() ? response.getData() : null;
        } catch (Exception e) {
            log.error("사용자 조회 실패: userId={}, error={}", userId, e.getMessage());
            return null;
        }
    }
}

 

우리가 사용하지 않는 WebFlux의 진짜 모습

// 실제 WebFlux 스타일 (우리는 사용 안함)
@RestController
public class ReactiveUserController {
    
    @GetMapping("/users/{id}")
    public Mono<UserResponse> getUser(@PathVariable Long id) {
        return userService.findUser(id)
            .map(UserResponse::from)
            .doOnError(error -> log.error("Error: {}", error.getMessage()));
    }
    
    @GetMapping("/users")
    public Flux<UserResponse> getUsers() {
        return userService.findAllUsers()
            .map(UserResponse::from)
            .take(100);
    }
}

현실: 우리는 단순히 HTTP 클라이언트로만 WebClient를 사용하고 있습니다.


톰캣 vs 네티: 서버 엔진의 차이점

톰캣 (Spring MVC 기본)

🏢 톰캣 = 전통적인 식당

👨‍🍳 요리사1 ← 손님1 (주문 처리 중...)
👨‍🍳 요리사2 ← 손님2 (주문 처리 중...)  
👨‍🍳 요리사3 ← 손님3 (주문 처리 중...)
👨‍🍳 요리사4   (대기 중)

특징:
- 한 요리사(스레드)가 한 손님을 끝까지 담당
- 요리사가 200명이면 동시에 200명만 처리 가능
- 요리사가 놀고 있어도 다른 일 못함

 

네티 (WebFlux 기본)

🏢 네티 = 현대적인 패스트푸드점

👨‍🍳 매니저1: 주문받기 → 조리팀에 전달 → 다음 손님
👨‍🍳 매니저2: 포장하기 → 손님에게 전달 → 다음 업무
👨‍🍳 매니저3: 음료 제작 → 완성되면 알림 → 다른 일

특징:
- 소수의 매니저(스레드)가 여러 업무를 순환하며 처리
- 기다리는 시간에 다른 일을 함
- 동시에 수천 명의 주문 처리 가능

 

실제 코드로 보는 차이점

Spring MVC (톰캣)

@RestController
public class UserController {
    
    @GetMapping("/users/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        // 1. DB 조회 (100ms 대기) ← 스레드가 100ms 동안 멈춤
        User user = userService.findById(id);
        
        // 2. 외부 API 호출 (200ms 대기) ← 스레드가 200ms 동안 또 멈춤  
        Profile profile = externalService.getProfile(id);
        
        // 3. 응답 생성
        return UserResponse.from(user, profile);
    }
    // 총 300ms 동안 이 스레드는 다른 요청 처리 불가
}

 

WebFlux (네티)

@RestController
public class ReactiveUserController {
    
    @GetMapping("/users/{id}")
    public Mono<UserResponse> getUser(@PathVariable Long id) {
        return userService.findById(id)  // DB 조회 시작 → 스레드 반환
            .flatMap(user -> 
                externalService.getProfile(id)  // API 호출 시작 → 스레드 반환
                    .map(profile -> UserResponse.from(user, profile))
            );
    }
    // 스레드는 즉시 다른 요청 처리 가능
}

성능 차이: 실제 수치로 이해하기

일반적인 웹 애플리케이션 시나리오

상황: 동시 사용자 1000명, 각 요청당 평균 300ms 소요

톰캣 방식:
- 스레드 풀: 200개
- 처리 가능한 동시 요청: 200개
- 나머지 800명은 대기
- 서버 메모리 사용량: 높음 (스레드당 1MB)

네티 방식:  
- 이벤트 루프: 8개 (CPU 코어 수)
- 처리 가능한 동시 요청: 1000개+
- 모든 사용자 즉시 처리 시작
- 서버 메모리 사용량: 낮음
 

실제 기업에서는 어떻게 사용할까?

대부분의 일반적인 서비스

// 대부분의 B2B, 사내 시스템, 중소규모 서비스
@RestController  // Spring MVC 사용
public class OrderController {
    
    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        Order order = orderService.createOrder(request);
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

현실:
- 동시 사용자: 100~1000명
- 평균 응답시간: 200~500ms  
- 톰캣으로 충분히 처리 가능
- 개발 복잡도: 낮음

 

대규모 트래픽 서비스

// Netflix, 카카오톡, 배달의민족 같은 대규모 서비스
@RestController  // WebFlux 사용
public class ReactiveNotificationController {
    
    @PostMapping("/notifications")
    public Mono<Void> sendNotification(@RequestBody NotificationRequest request) {
        return notificationService.sendToAllUsers(request)
            .then();
    }
}

현실:
- 동시 사용자: 수만~수십만명
- 외부 API 호출 빈번
- I/O 대기시간이 성능 병목
- WebFlux로 5~10배 성능 향상

 

실제 기업 사례

WebFlux를 사용하는 경우

  • Netflix: 마이크로서비스 간 대량 통신
  • Spring Cloud Gateway: API 게이트웨이 (높은 처리량 필요)
  • 카카오페이: 실시간 결제 처리

Spring MVC를 사용하는 경우

  • 대부분의 쇼핑몰: 일반적인 CRUD
  • 사내 관리 시스템: 동시 사용자 수백명 이하
  • 중소 규모 API: 단순한 비즈니스 로직

우리 프로젝트에서 WebFlux를 사용하는 이유

현재 상황

// 우리의 WebClient 사용 (WebFlux 의존성 필요)
@Component
public class UserClient {
    private final WebClient webClient;  // ← 이것 때문에 WebFlux 의존성 추가
    
    public UserClientResponseDto getUser(Long userId) {
        return webClient.get()
            .uri("/api/v2/users/{userId}", userId)
            .retrieve()
            .bodyToMono(UserClientResponseDto.class)
            .block();  // ← 하지만 결국 동기적으로 사용
    }
}

대안과 비교

1. RestTemplate (기존 방식)

@Component  
public class UserClient {
    private final RestTemplate restTemplate;
    
    public UserClientResponseDto getUser(Long userId) {
        return restTemplate.getForObject(
            "/api/v2/users/{userId}", 
            UserClientResponseDto.class, 
            userId
        );
    }
}

장점: 간단함, WebFlux 의존성 불필요
단점: Spring Boot 2.x부터 유지보수 모드 (deprecated 예정)

 

2. WebClient (현재 방식)

// 현재 우리 방식
public UserClientResponseDto getUser(Long userId) {
    return webClient.get()
        .uri("/api/v2/users/{userId}", userId)  
        .retrieve()
        .bodyToMono(UserClientResponseDto.class)
        .block();
}

장점: 최신 표준, 더 많은 기능
단점: WebFlux 의존성 필요, 약간의 오버헤드

결론: 우리가 알아야 할 것

현재 우리 팀의 인식 수준

✅ 알아야 할 것:
- WebClient는 최신 HTTP 클라이언트
- 우리는 WebFlux의 5%도 사용하지 않음
- 네티 vs 톰캣의 기본적인 차이점
- 대부분의 서비스는 톰캣으로 충분함

❌ 지금 당장 알 필요 없는 것:
- Mono, Flux의 복잡한 연산자들
- 백프레셰어(Backpressure) 제어
- Reactor 프로그래밍 패러다임
- 완전한 반응형 스택 구성

 

실무적 조언

  1. 현재 프로젝트:
    • WebClient만 사용하면 충분
    • .block()을 써서 동기적으로 사용해도 OK
    • 성능상 문제없음
  2. 미래 학습 방향:
    • 먼저 Spring MVC 완전 숙달
    • 대규모 트래픽 처리가 필요할 때 WebFlux 고려
    • 회사에서 요구할 때 깊이 있게 학습
  3. 기술 선택 기준:
     
    동시 사용자 < 1000명 → Spring MVC
    외부 API 호출 적음 → Spring MVC  
    단순한 CRUD → Spring MVC
    
    동시 사용자 > 5000명 → WebFlux 고려
    외부 API 호출 많음 → WebFlux 고려
    실시간 스트리밍 → WebFlux 필수

마무리

우리는 지금 올바른 도구를 올바른 목적으로 사용하고 있습니다. WebClient는 현재 Spring에서 권장하는 HTTP 클라이언트이고, 우리의 도메인 간 통신 목적에 맞습니다.

WebFlux의 복잡한 반응형 프로그래밍을 모르더라도, "우리는 최신 기술을 적절한 수준에서 활용하고 있다"는 정도로 이해하시면 충분합니다.

실제 현업에서도 대부분의 개발자들이 이 정도 수준에서 WebFlux를 활용하고 있으니, 너무 부담가지지 마시기 바랍니다.