들어가며
우리 프로젝트에서는 도메인 간 통신을 위해 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 프로그래밍 패러다임
- 완전한 반응형 스택 구성
실무적 조언
- 현재 프로젝트:
- WebClient만 사용하면 충분
- .block()을 써서 동기적으로 사용해도 OK
- 성능상 문제없음
- 미래 학습 방향:
- 먼저 Spring MVC 완전 숙달
- 대규모 트래픽 처리가 필요할 때 WebFlux 고려
- 회사에서 요구할 때 깊이 있게 학습
- 기술 선택 기준:
동시 사용자 < 1000명 → Spring MVC 외부 API 호출 적음 → Spring MVC 단순한 CRUD → Spring MVC 동시 사용자 > 5000명 → WebFlux 고려 외부 API 호출 많음 → WebFlux 고려 실시간 스트리밍 → WebFlux 필수
마무리
우리는 지금 올바른 도구를 올바른 목적으로 사용하고 있습니다. WebClient는 현재 Spring에서 권장하는 HTTP 클라이언트이고, 우리의 도메인 간 통신 목적에 맞습니다.
WebFlux의 복잡한 반응형 프로그래밍을 모르더라도, "우리는 최신 기술을 적절한 수준에서 활용하고 있다"는 정도로 이해하시면 충분합니다.
실제 현업에서도 대부분의 개발자들이 이 정도 수준에서 WebFlux를 활용하고 있으니, 너무 부담가지지 마시기 바랍니다.
'Spring 7기 프로젝트 > 모임 플렛폼 프로젝트' 카테고리의 다른 글
| ELK 스택을 활용한 애플리케이션 모니터링 시스템 구축 (1) | 2025.07.28 |
|---|---|
| 로드밸런서 완벽 가이드: 동작 원리부터 MSA 전환 전략까지 (3) | 2025.07.25 |
| Elasticsearch 완벽 가이드: 실무에서 알아야 할 모든 것 (1) | 2025.07.25 |
| 도메인 경계 분리를 통한 Auth와 User 도메인 리팩토링 (0) | 2025.07.24 |
| List.containsAll() vs HashSet.containsAll() 성능 비교 (0) | 2025.07.24 |