WebMVC vs WebFlux
스프링이 지원하는 웹 프레임워크는 두 가지이다. 바로 이 글의 타이틀인 WebMVC와 WebFlux가 그 프레임워크인데, 이렇게 스프링이 두 가지 웹 프레임워크를 지원하는 이유는 각각 서로 다른 아키텍처 패러다임에 최적화되어 있기 때문이다.
처음부터 두 프레임워크가 함께 존재했던 것은 아니다. WebMVC가 먼저 등장했고, 시간이 흐르면서 WebFlux가 추가되었는데, 그 배경에는 기술적 변화와 도전이 있었다. 그렇다면 WebFlux가 항상 더 나은 선택일까? 반드시 그렇지는 않다. 단순히 새로운 기술을 맹목적으로 따르는 것이 아니라, 왜 그 기술이 등장했는지 배경을 이해해야 기술 선택에 있어 더 현명한 접근을 할 수 있다.
Spring Framework History 훑어보기
Spring WebMVC는 2004년 Spring 1.0과 함께 등장했다. 당시에는 서블릿 기반의 동기적 처리 방식이 웹 개발의 표준이었고, 대부분의 애플리케이션에서 충분히 효과적으로 작동했다.
1
2
3
4
5
// 전통적인 WebMVC - 블로킹 방식
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id) {
return userService.findById(id); // DB 호출 시 스레드 블로킹
}
하지만 2010년대 초부터 문제점들이 드러나기 시작했다. 동시 연결 1만개를 처리하는 C10K 문제가 대두되었고, 요청마다 스레드를 생성하는 스레드 풀 방식으로 인한 메모리 사용량 증가 문제가 발생했다. 특히 데이터베이스나 외부 API 호출 시 스레드가 대기 상태로 머물면서 자원이 낭비되는 블로킹 I/O의 한계가 명확해졌다.
외부에서 불어온 변화의 바람
2009년 등장한 Node.js는 이벤트 루프 기반의 비동기 처리로 높은 동시성을 달성하며 Java 진영에 큰 충격을 주었다. “JavaScript가 Java보다 빠르다고?”라는 충격적인 벤치마크 결과들이 나오기 시작했다.
동시에 리액티브 프로그래밍이 트렌드로 떠오르면서 Netflix의 RxJava(2013), Reactive Streams 스펙(2015)이 연이어 발표되었다. 마이크로서비스 아키텍처가 확산되면서 높은 동시성에 대한 요구도 급증했다.
Spring 팀의 고민과 WebFlux의 탄생
Spring 팀은 딜레마에 빠졌다. 기존 WebMVC를 개선하는 것만으로는 한계가 있었다. 서블릿 API 자체가 블로킹 기반으로 설계되어 있어 근본적인 한계가 존재했기 때문이다. 결국 Spring 팀은 별도의 프로젝트를 개설하게 되며 2017년 Spring 5.0 부터 WebFlux가 지원되기 시작했다.
그럼 WebFlux의 논블로킹 설계는 어떻게 이루어져 있을까? 세부적으로 어떤 점 때문에 WebMVC에 한계가 있다는 걸까?
두 프레임워크의 설계 철학
WebMVC의 아키텍처 설계 원칙
WebMVC는 Thread-per-Request 모델을 핵심 아키텍처 패러다임으로 채택하고 있다. 이는 서블릿 API의 동기적 처리 모델을 기반으로 한 설계 철학에서 비롯된 것으로, 클라이언트의 하나의 요청 당 하나의 스레드를 할당해 해당 요청을 처리하는 방식이다.
이러한 접근 방식으로 요청부터 응답까지의 선형적 처리 흐름을 중심으로 한 아키텍처가 구성되었으며 각 요청이 명확하고 예측 가능한 실행 경로를 따라 처리되도록 보장한다.
하나의 스레드가 하나의 요청을 전담한다는 직관적이고 단순한 철학은 개발자들이 코드의 흐름을 쉽게 이해하고 디버깅할 수 있게 해주며, 전통적인 동기 프로그래밍 모델과 자연스럽게 일치한다.
WebFlux의 아키텍처 설계 원칙
WebFlux는 Event-Loop 모델을 핵심 아키텍처 패러다임으로 삼고 있다. 이는 리액티브 스트림의 비동기적 처리 모델을 기반으로 한 설계 철학으로, 소수의 이벤트 루프 스레드가 여러 요청을 번갈아가며 처리하는 방식을 구현한다.
데이터 흐름의 반응형 처리를 중심으로 한 아키텍처로써 요청이 들어오면 즉시 처리를 시작하지만, I/O 작업이 필요한 순간에는 해당 작업을 비동기적으로 시작하고 스레드는 다른 요청을 처리하러 이동한다. 이후 I/O 작업이 완료되면 이벤트나 콜백을 통해 알림을 받아 처리를 재개하는 방식으로 동작한다.
“적은 자원으로 많은 요청을 효율적으로 처리한다.”는 최적화된 철학은 높은 동시성과 자원 효율성을 달성하는 것을 목표로 하며, 특히 I/O 집약적인 애플리케이션에서 그 진가를 발휘한다.
아키텍처 차이
근본적인 처리 모델의 차이
WebMVC와 WebFlux의 가장 근본적인 차이는 Thread-per-Request와 Event-Loop 방식의 차이에 있다.
WebMVC는 각 요청마다 전담 스레드를 할당하여 요청부터 응답까지 스레드가 해당 작업에 묶여있는 상태가 된다. 반면 WebFlux는 소수의 이벤트 루프 스레드가 여러 요청을 번갈아가며 처리하여 스레드가 절대 유휴 상태에 있지 않도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// WebMVC - Thread-per-Request 방식
@RestController
public class WebMvcController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable String id) {
// 1. 스레드가 요청에 전담 할당
User user = userService.findById(id); // DB 호출 시 스레드 블로킹
// 2. 외부 API 호출 시에도 스레드 대기
Profile profile = externalApiService.getProfile(id); // 스레드 블로킹
// 3. 응답까지 동일 스레드가 처리
user.setProfile(profile);
return user; // 스레드 해제
}
}
// WebFlux - Event-Loop 방식
@RestController
public class WebFluxController {
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
return userService.findById(id) // 1. 비동기 DB 호출 시작, 스레드는 다른 작업으로
.flatMap(user ->
externalApiService.getProfile(id) // 2. 비동기 API 호출, 스레드는 다른 작업으로
.map(profile -> {
user.setProfile(profile);
return user; // 3. 결과 조합 후 반환
})
);
// 이벤트 루프 스레드는 계속해서 다른 요청들을 처리
}
}
동시성 처리 방식의 차이
WebMVC는 동시 처리량을 높이려면 스레드 수 증가나 하드웨어 성능 향상에 의존하게 되며, 각 스레드는 상당한 메모리를 차지한다. WebFlux는 소수의 이벤트 루프 스레드로 여러 요청을 처리하기 때문에 같은 하드웨어에서 더 많은 동시 요청을 감당할 수 있고, 결과적으로 서버를 수평 확장할 때도 더 적은 인스턴스로 동일한 처리량을 달성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// WebMVC - 1000개 동시 요청 처리
public class WebMvcLoadTest {
// 1000개 요청 = 최대 1000개 스레드 필요
// 각 스레드당 약 1MB 메모리 사용 (스택 크기)
// 총 메모리 사용량: 약 1GB + 힙 메모리
@GetMapping("/heavy-operation")
public ResponseEntity<String> heavyOperation() {
// 스레드가 전담 처리
String result = performDatabaseQuery(); // 블로킹
String apiResult = callExternalApi(); // 블로킹
return ResponseEntity.ok(result + apiResult);
}
}
// WebFlux - 1000개 동시 요청 처리
public class WebFluxLoadTest {
// 1000개 요청을 4-8개 이벤트 루프 스레드로 처리
// 총 메모리 사용량: 스레드 수 * 1MB + 힙 메모리 (대폭 절약)
@GetMapping("/heavy-operation")
public Mono<ResponseEntity<String>> heavyOperation() {
Mono<String> dbResult = performDatabaseQuery(); // 이 시점에서는 아직 실행되지 않음 (Cold Publisher)
Mono<String> apiResult = callExternalApi(); // 이 시점에서도 아직 실행되지 않음 (Cold Publisher)
return Mono.zip(dbResult, apiResult) // 구독(subscribe) 시점에 두 작업이 병렬로 시작됨
.map(tuple -> ResponseEntity.ok(tuple.getT1() + tuple.getT2()));
}
}
I/O 처리 방식의 차이
I/O 작업에서 두 프레임워크의 차이가 극명하게 드러난다.
WebMVC는 기본적으로 각 I/O 작업마다 스레드가 대기하며 순차적으로 처리한다. CompletableFuture나 @Async 등을 활용하면 병렬 처리도 가능하지만, 별도의 스레드 풀 관리가 필요하고 코드 복잡도가 증가한다. 반면 WebFlux는 모든 I/O를 기본적으로 비동기 처리하여 별도의 설정 없이도 병렬성을 극대화한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// WebMVC - 순차적 I/O 처리
@Service
public class WebMvcService {
public UserDetails getUserDetails(String userId) {
// 1. DB에서 사용자 조회 (500ms) - 스레드 블로킹
User user = userRepository.findById(userId);
// 2. 프로필 API 호출 (300ms) - 스레드 블로킹
Profile profile = profileApiClient.getProfile(userId);
// 3. 권한 API 호출 (200ms) - 스레드 블로킹
Permissions permissions = permissionApiClient.getPermissions(userId);
// 총 소요 시간: 500 + 300 + 200 = 1000ms
// 스레드는 1000ms 동안 블로킹됨
return new UserDetails(user, profile, permissions);
}
}
// WebFlux - 병렬 I/O 처리
@Service
public class WebFluxService {
public Mono<UserDetails> getUserDetails(String userId) {
Mono<User> userMono = userRepository.findById(userId); // 비동기 DB 조회
Mono<Profile> profileMono = profileApiClient.getProfile(userId); // 비동기 API 호출
Mono<Permissions> permissionsMono = permissionApiClient.getPermissions(userId); // 비동기 API 호출
// 세 작업이 병렬로 실행됨
return Mono.zip(userMono, profileMono, permissionsMono)
.map(tuple -> new UserDetails(tuple.getT1(), tuple.getT2(), tuple.getT3()));
// 총 소요 시간: max(500, 300, 200) = 500ms
// 이벤트 루프 스레드는 다른 작업을 계속 처리
}
}
메모리 사용 패턴의 차이
메모리 사용에서도 두 프레임워크는 상반된 특성을 보인다.
WebMVC는 스레드 수에 비례한 메모리 사용을 보이며, WebFlux는 일정한 메모리 사용량을 유지하면서 백프레셔를 통해 메모리를 제어한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// WebMVC - 대용량 데이터 처리
@RestController
public class WebMvcDataController {
@GetMapping("/large-dataset")
public List<Data> getLargeDataset() {
// 100만 건 데이터를 모두 메모리에 로드
List<Data> allData = dataRepository.findAll(); // OutOfMemoryError 위험
// 동시 요청이 많을수록 메모리 사용량 선형 증가
// 1000개 동시 요청 × 100만 건 데이터 = 메모리 폭발
return allData;
}
}
// WebFlux - 스트리밍 방식 처리
@RestController
public class WebFluxDataController {
@GetMapping(value = "/large-dataset", produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<Data> getLargeDataset() {
return dataRepository.findAll() // 스트리밍 방식
.buffer(1000) // 배치 단위로 처리
.flatMap(batch -> processBatch(batch), 2) // 동시 실행 제한
.onBackpressureBuffer(5000, // 백프레셔로 메모리 제어
data -> log.warn("Dropping data: {}", data.getId()),
BufferOverflowStrategy.DROP_OLDEST);
// 메모리 사용량이 일정 수준으로 제한됨
// 동시 요청이 증가해도 메모리 사용량 안정적
}
}
에러 처리와 복구 전략
에러 처리에서도 두 프레임워크는 다른 철학을 보인다.
WebMVC는 예외 기반의 동기적 에러 처리를, WebFlux는 함수형 방식의 비동기 에러 처리를 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// WebMVC - 예외 기반 에러 처리
@RestController
public class WebMvcErrorController {
@GetMapping("/risky-operation/{id}")
public ResponseEntity<String> riskyOperation(@PathVariable String id) {
try {
String result = externalService.call(id); // 실패 시 예외 발생
return ResponseEntity.ok(result);
} catch (ServiceException e) {
// 동기적 에러 처리
log.error("Service call failed", e);
return ResponseEntity.status(500).body("Service unavailable");
} catch (TimeoutException e) {
// 각 예외를 개별적으로 처리
return ResponseEntity.status(408).body("Request timeout");
}
}
}
// WebFlux - 함수형 에러 처리
@RestController
public class WebFluxErrorController {
@GetMapping("/risky-operation/{id}")
public Mono<ResponseEntity<String>> riskyOperation(@PathVariable String id) {
return externalService.call(id)
.map(result -> ResponseEntity.ok(result))
.onErrorResume(ServiceException.class, e -> {
// 비동기 에러 처리 및 대안 실행
log.error("Service call failed, trying fallback", e);
return fallbackService.call(id)
.map(fallback -> ResponseEntity.ok("Fallback: " + fallback))
.onErrorReturn(ResponseEntity.status(500).body("All services unavailable"));
})
.onErrorResume(TimeoutException.class, e -> {
// 재시도 로직
return externalService.call(id)
.retry(2)
.map(result -> ResponseEntity.ok(result))
.onErrorReturn(ResponseEntity.status(408).body("Request timeout after retries"));
})
.timeout(Duration.ofSeconds(10)); // 선언적 타임아웃 설정
}
}
데이터베이스 연결 관리의 패러다임
WebMVC: Connection Pool 기반 관리
WebMVC에서는 전통적인 JDBC Connection Pool 방식을 사용한다. 각 스레드가 커넥션을 점유하는 동안 해당 커넥션은 다른 작업에 사용될 수 없어, 커넥션 수가 곧 동시 처리 가능한 요청 수가 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// WebMVC - 전통적인 JDBC 방식
@Repository
public class WebMvcUserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public User findById(String id) {
// 스레드가 커넥션을 점유하며 쿼리 실행
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
new Object[]{id},
new BeanPropertyRowMapper<>(User.class)
); // 쿼리 완료까지 커넥션과 스레드 모두 블로킹
}
@Transactional
public User updateUser(String id, User user) {
// 트랜잭션 동안 커넥션 점유 지속
jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?",
user.getName(), id);
return findById(id); // 같은 트랜잭션 내에서 커넥션 재사용
}
}
WebFlux: R2DBC를 통한 리액티브 데이터베이스 액세스
WebFlux는 R2DBC(Reactive Relational Database Connectivity)를 통해 비동기 데이터베이스 액세스를 제공한다. 커넥션을 이벤트 루프 간에 효율적으로 공유하여 적은 수의 커넥션으로도 높은 처리량을 달성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WebFlux - R2DBC 방식
@Repository
public class WebFluxUserRepository {
@Autowired
private R2dbcEntityTemplate template;
public Mono<User> findById(String id) {
return template.selectOne(
Query.query(where("id").is(id)),
User.class
); // 비동기 쿼리, 커넥션 즉시 반환
}
@Transactional
public Mono<User> updateUser(String id, User user) {
return template.update(
Query.query(where("id").is(id)),
Update.update("name", user.getName()),
User.class
).then(findById(id)); // 트랜잭션 내 비동기 체이닝
}
}
커넥션 풀 효율성 비교
- WebMVC - 1000개 동시 요청
- 스레드 수: 최대 1000개
- 커넥션 풀 크기: 일반적으로 10~50개
- 스레드가 커넥션을 점유하는 시간이 길어 병목 발생
- 커넥션 대기(pool exhaustion)로 인한 응답 지연 가능성
- WebFlux - 1000개 동시 요청
- 이벤트 루프 스레드: 4~8개
- 커넥션 풀 크기: 10~20개로 충분
- 커넥션을 비동기로 공유하여 점유 시간 최소화
- 동일한 커넥션 수로 훨씬 높은 처리량 달성
WebMVC에서는 스레드가 쿼리 실행부터 결과 수신까지 커넥션을 점유하기 때문에, 동시 요청이 늘어날수록 커넥션 대기 시간이 길어진다. 반면 WebFlux는 비동기 I/O로 커넥션을 쿼리 실행 중에만 사용하고 즉시 반환하기 때문에, 같은 수의 커넥션으로 훨씬 높은 처리량을 달성할 수 있다.
백프레셔(Backpressure) 지원과 시스템 안정성
WebMVC의 한계: 고정된 처리 용량
WebMVC는 스레드 풀 크기로 처리 용량이 결정되며, 용량 초과 시 큐잉이나 요청 거부만 가능하다. 그래서 시스템 부하가 증가해도 적응적으로 대응하기 어렵다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WebMVC - 백프레셔 부재로 인한 문제
@RestController
public class WebMvcStreamController {
@GetMapping("/data-stream")
public ResponseEntity<List<Data>> getDataStream() {
// 생산자가 빠르고 소비자가 느린 경우
List<Data> allData = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
Data data = heavyProcessing(i); // 빠른 생산
allData.add(data);
}
// 메모리에 모든 데이터 적재 - OutOfMemoryError 위험
return ResponseEntity.ok(allData);
}
}
WebFlux의 백프레셔: 시스템 자기보호 메커니즘
WebFlux는 Reactive Streams 스펙에 따라 다양한 백프레셔 전략을 제공하여 시스템이 과부하 상황에서도 안정성을 유지할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// WebFlux - 다양한 백프레셔 전략
@RestController
public class WebFluxStreamController {
@GetMapping(value = "/data-stream", produces = MediaType.APPLICATION_NDJSON_VALUE)
public Flux<Data> getDataStream() {
return Flux.range(1, 1000000)
.map(this::heavyProcessing)
.onBackpressureBuffer(1000, // 버퍼 크기 제한
data -> log.warn("Buffer overflow, dropping: {}", data.getId()),
BufferOverflowStrategy.DROP_OLDEST) // 오래된 데이터 드롭
.delayElements(Duration.ofMillis(10)); // 처리 속도 조절
}
@GetMapping("/adaptive-stream")
public Flux<Data> getAdaptiveStream() {
return dataService.generateData()
.onBackpressureLatest() // 최신 데이터만 유지
.sample(Duration.ofSeconds(1)) // 샘플링으로 부하 조절
.doOnRequest(n -> log.info("Downstream requested: {} items", n))
.doOnCancel(() -> log.info("Downstream cancelled subscription"));
}
}
트랜잭션 처리의 근본적 차이
WebMVC: ThreadLocal 기반 트랜잭션 전파
WebMVC는 ThreadLocal을 활용한 트랜잭션 관리로 하나의 스레드에서 모든 트랜잭션 정보가 공유되어 직관적이고 안전하게 작동한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// WebMVC - ThreadLocal 기반 트랜잭션 전파
@Service
@Transactional
public class WebMvcTransactionService {
@Autowired
private UserRepository userRepository;
@Autowired
private OrderRepository orderRepository;
public Order processOrder(String userId, OrderRequest request) {
// ThreadLocal에 트랜잭션 정보 저장됨
// TransactionSynchronizationManager.getCurrentTransactionName()
// 동일 스레드에서 실행되는 모든 코드가 트랜잭션 정보 공유
User user = userRepository.findById(userId); // ThreadLocal에서 트랜잭션 정보 획득
if (user.getBalance() < request.getAmount()) {
throw new InsufficientBalanceException(); // 롤백 발생
}
user.deductBalance(request.getAmount());
userRepository.save(user); // 같은 ThreadLocal 트랜잭션 사용
Order order = new Order(userId, request);
return orderRepository.save(order); // 같은 ThreadLocal 트랜잭션 사용
// 메서드 종료 시 ThreadLocal에서 트랜잭션 정보 제거 후 커밋
}
}
WebFlux: Reactor Context 기반 트랜잭션 전파
WebFlux에서는 스레드가 계속 바뀌기 때문에 ThreadLocal 기반의 트랜잭션이 작동하지 않는다. 대신 Reactor Context를 통해 비동기 체인 전체에서 트랜잭션 정보를 전파한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// WebFlux - Reactor Context 기반 트랜잭션 전파
@Service
public class WebFluxTransactionService {
@Autowired
private ReactiveUserRepository userRepository;
@Autowired
private ReactiveOrderRepository orderRepository;
@Transactional // Reactor Context에 TransactionContext 저장
public Mono<Order> processOrder(String userId, OrderRequest request) {
// @Transactional AOP가 자동으로 다음과 같이 동작:
// .contextWrite(TransactionContextManager.getOrCreateContext())
return userRepository.findById(userId) // Reactor Context에서 트랜잭션 정보 획득
.flatMap(user -> {
if (user.getBalance() < request.getAmount()) {
return Mono.error(new InsufficientBalanceException());
}
user.deductBalance(request.getAmount());
return userRepository.save(user) // 모든 체인에서 Context 자동 전파
.then(orderRepository.save(new Order(userId, request)));
});
// 구독 완료 시 Reactor Context에서 트랜잭션 정보 제거 후 커밋
}
// 수동으로 TransactionalOperator 사용하는 경우
@Autowired
private TransactionalOperator transactionalOperator;
public Mono<TransactionResult> manualTransactionControl(String userId) {
return performBusinessLogic(userId)
.doOnNext(result -> log.info("Business logic completed"))
.as(transactionalOperator::transactional) // 수동으로 Reactor Context에 트랜잭션 바인딩
.doOnSuccess(result -> log.info("Transaction will commit"))
.doOnError(error -> log.error("Transaction will rollback", error));
}
}
만약 WebFlux가 트랜잭션을 어떻게 가능하게 하는지 더 자세한 내용을 알고 싶다면 링크에서 참고해주길 바란다.
모니터링과 관찰 가능성(Observability)
WebMVC: 전통적인 메트릭 수집
WebMVC는 스레드 기반 모니터링이 직관적이며, 기존 APM 도구들과 잘 연동된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// WebMVC - 스레드 풀 기반 모니터링
@Component
public class WebMvcMonitoring {
@EventListener
public void handleRequest(ServletRequestHandledEvent event) {
// 스레드별 처리 시간 측정
long processingTime = event.getProcessingTimeMillis();
String threadName = Thread.currentThread().getName();
log.info("Request processed by thread: {}, time: {}ms",
threadName, processingTime);
// JVM 스레드 메트릭
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
log.info("Active threads: {}, Peak threads: {}",
threadBean.getThreadCount(), threadBean.getPeakThreadCount());
}
}
WebFlux: 리액티브 스트림 메트릭
WebFlux는 스트림 기반 메트릭이 필요하며, Micrometer와의 깊은 통합을 통해 리액티브 특화 모니터링을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// WebFlux - 리액티브 메트릭 수집
@Component
public class WebFluxMonitoring {
@Autowired
private MeterRegistry meterRegistry;
public Mono<String> monitoredService(String input) {
return Mono.fromCallable(() -> processInput(input))
.name("business.process") // 메트릭 이름
.tag("input.type", input.substring(0, 1)) // 태그 추가
.metrics() // Micrometer 메트릭 자동 수집
.doOnSubscribe(subscription ->
meterRegistry.counter("requests.started").increment())
.doOnNext(result ->
meterRegistry.counter("requests.completed").increment())
.doOnError(error ->
meterRegistry.counter("requests.failed",
"error", error.getClass().getSimpleName()).increment())
.elapsed() // 처리 시간 측정
.doOnNext(tuple ->
meterRegistry.timer("processing.time")
.record(tuple.getT1(), TimeUnit.MILLISECONDS));
}
}
결론
두 프레임워크의 선택 기준을 정리해보면, WebMVC는 팀의 Spring MVC 경험이 풍부하거나, 복잡한 트랜잭션 로직이 많고, 빠른 개발과 출시가 우선인 경우에 적합하다. 기존 JDBC 라이브러리에 대한 의존성이 크거나 예측 가능한 성능 요구사항을 가진 관리자 도구, 내부 시스템, 전통적인 웹 애플리케이션에 잘 맞는다.
반면 WebFlux는 높은 동시성 처리가 필요하거나 I/O 집약적인 워크로드를 다루는 경우에 강점을 보인다. 마이크로서비스 아키텍처, 스트리밍이나 실시간 데이터 처리, 클라우드 환경에서 리소스 효율성이 중요한 API 게이트웨이나 대용량 트래픽 API에 적합하다.
지금까지 살펴보았듯이 두 프레임워크는 경쟁 관계가 아닌 상호 보완적 관계이며 우리는 비즈니스 요구사항과 팀의 역량, 시스템의 특성을 종합적으로 고려하여 두 기술 모두를 사용할 수 있다. 역시 “Silver Bullet은 없다”. 각 상황에 맞는 최적의 선택을 하는데에 이 글이 도움이 되길 바라며 마치겠다.