Spring MVC에서 동시 요청이 몰리면 스레드 풀이 고갈되는 건 왜일까? 그리고 WebFlux는 이 문제를 어떻게 해결하는 걸까?

블로킹 I/O의 한계부터 Reactive Streams, 그리고 실무에서 WebFlux를 진짜 써야 하는 상황까지 풀어볼게요.

왜 리액티브인가

스레드 풀 고갈 문제

Spring MVC는 기본적으로 요청 하나당 스레드 하나 를 할당하는 모델이에요. Tomcat 기본 스레드 풀이 200개니까, 동시에 200개 넘는 요청이 들어오면 나머지는 큐에서 대기해야 합니다.

문제는 대부분의 서버 작업이 I/O 대기 라는 점이에요. DB 쿼리 날리고 응답 올 때까지, 외부 API 호출하고 결과 받을 때까지 — 스레드가 아무것도 안 하면서 그냥 잡고 있습니다. CPU는 놀고 있는데 스레드가 부족해서 요청을 못 받는 상황이 생겨요.

PLAINTEXT
[요청 1] ──── DB 쿼리 ──── (대기 중...) ──── 응답 처리 ──── 완료
[요청 2] ──── API 호출 ──── (대기 중...) ──── 응답 처리 ──── 완료
[요청 3] ──── (스레드 풀 가득 참, 대기 중...)
...
[요청 201] ──── 503 Service Unavailable

C10K 문제

이건 1999년에 제기된 문제인데, 핵심은 간단해요. 하나의 서버에서 동시 1만 개 커넥션을 처리할 수 있느냐 는 거죠. 스레드 하나당 1MB 스택 메모리를 잡으면, 1만 스레드 = 10GB 메모리. 컨텍스트 스위칭 비용까지 합치면 사실상 불가능합니다.

이 문제의 해법이 논블로킹 I/O 예요. I/O 작업을 시작하고 완료될 때까지 스레드를 블로킹하지 않습니다. 대신 완료되면 콜백이나 이벤트로 알려주는 방식이에요. 이러면 소수의 스레드로도 수만 개의 커넥션을 처리할 수 있습니다.

Node.js가 싱글 스레드 이벤트 루프로 높은 동시성을 달성하는 것도 같은 원리다. Java 쪽에서 이걸 제대로 해보겠다는 게 리액티브 프로그래밍이다.


Reactive Streams 스펙

리액티브 프로그래밍을 하려면 표준이 필요합니다. 각 라이브러리(RxJava, Reactor, Akka Streams)가 제각각이면 호환이 안 되니까요. 그래서 나온 게 Reactive Streams 스펙이에요. Java 9의 java.util.concurrent.Flow에도 이 인터페이스가 들어갔습니다.

핵심 인터페이스는 딱 4개예요.

Publisher

JAVA
public interface Publisher<T> {
    void subscribe(Subscriber<? super T> s);
}

데이터를 발행하는 쪽이에요. subscribe()를 호출하면 데이터 스트림이 시작됩니다. 중요한 건, ** 구독하기 전까지는 아무 일도 일어나지 않는다 **는 점이에요. 이걸 cold 방식이라고 해요.

Subscriber

JAVA
public interface Subscriber<T> {
    void onSubscribe(Subscription s);
    void onNext(T t);
    void onError(Throwable t);
    void onComplete();
}

데이터를 받아서 처리하는 쪽이에요. onSubscribe()로 구독이 시작되고, onNext()로 데이터를 하나씩 받고, 끝나면 onComplete(), 에러 나면 onError()가 호출됩니다. onComplete()onError()는 터미널 시그널이라서 둘 중 하나만 호출돼요.

Subscription

JAVA
public interface Subscription {
    void request(long n);
    void cancel();
}

Publisher와 Subscriber 사이의 연결 고리예요. request(n)으로 "n개만큼 데이터를 보내줘"라고 요청하고, cancel()로 구독을 취소합니다. ** 백프레셔의 핵심이 바로 이 request()**예요.

Processor

JAVA
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

Subscriber이면서 동시에 Publisher인 녀석이에요. 중간에서 데이터를 받아서 변환하고 다시 내보내는 역할을 합니다. 실무에서 직접 구현할 일은 거의 없고, Reactor의 연산자들이 내부적으로 이 역할을 해요.

동작 흐름

PLAINTEXT
Subscriber → subscribe() → Publisher
Publisher  → onSubscribe(Subscription) → Subscriber
Subscriber → request(n) → Subscription
Publisher  → onNext(data) × n → Subscriber
Publisher  → onComplete() 또는 onError() → Subscriber

이 흐름을 그림으로 그려가며 설명할 수 있으면 리액티브의 핵심을 이해한 거예요.


백프레셔 (Backpressure)

왜 필요한가

생산자가 초당 10,000건을 쏟아내는데 소비자가 초당 100건밖에 처리 못하면 어떻게 될까요? 중간 버퍼가 터지거나 OOM이 납니다. 전통적인 push 모델(옵저버 패턴)에서는 이걸 제어할 방법이 없었어요.

백프레셔는 ** 소비자가 자기 처리 능력에 맞게 생산자에게 데이터 양을 조절해달라고 요청 **하는 메커니즘이에요. Reactive Streams의 Subscription.request(n)이 바로 그 수단입니다.

백프레셔 전략

Reactor에서 제공하는 대표적인 전략들이에요.

전략설명사용 상황
onBackpressureBuffer()처리 못한 데이터를 버퍼에 쌓는다일시적인 속도 차이가 있을 때
onBackpressureDrop()처리 못한 데이터를 그냥 버린다최신 데이터만 중요할 때 (센서 데이터 등)
onBackpressureLatest()가장 최근 데이터만 유지하고 나머지 버린다실시간 시세 같은 경우
onBackpressureError()소비자가 따라가지 못하면 에러를 던진다데이터 유실이 절대 안 되는 경우
JAVA
Flux.range(1, 1_000_000)
    .onBackpressureBuffer(1024)  // 버퍼 크기 제한
    .subscribe(
        data -> processSlowly(data),
        error -> log.error("버퍼 초과", error)
    );

** 한 줄 정리 **: 백프레셔는 소비자가 생산자에게 처리 가능한 양만큼만 데이터를 요청하는 흐름 제어 메커니즘이다. Reactive Streams에서는 Subscription.request(n)으로 구현된다.


Project Reactor

Spring WebFlux가 기본으로 사용하는 Reactive Streams 구현체예요. Pivotal(지금은 VMware)에서 만들었고, Spring 생태계와 완전히 통합되어 있습니다.

Mono와 Flux

Reactor의 핵심 타입은 딱 두 개예요.

타입발행 개수비유
Mono<T>0 또는 1개Optional<T>의 비동기 버전
Flux<T>0 ~ N개Stream<T>의 비동기 버전
JAVA
// Mono — 하나의 결과
Mono<User> user = userRepository.findById(1L);

// Flux — 여러 개의 결과
Flux<User> users = userRepository.findAll();

// 생성
Mono<String> mono = Mono.just("hello");
Mono<String> empty = Mono.empty();
Mono<String> error = Mono.error(new RuntimeException("fail"));

Flux<Integer> flux = Flux.just(1, 2, 3);
Flux<Integer> range = Flux.range(1, 10);
Flux<Long> interval = Flux.interval(Duration.ofSeconds(1));

중요한 건, Mono든 Flux든 ** 구독(subscribe)하기 전까지는 아무것도 실행되지 않는다 **는 점이에요. 이걸 cold sequence라고 합니다. subscribe()를 호출하거나 Spring WebFlux가 내부적으로 구독해줘야 데이터가 흐르기 시작해요.

주요 연산자

실무에서 자주 쓰는 연산자들을 정리해 볼게요.

map — 동기 변환

JAVA
Mono<String> name = userMono.map(user -> user.getName());

값을 동기적으로 변환할 때 씁니다. Java Stream의 map()과 같은 느낌이에요.

flatMap — 비동기 변환

JAVA
Mono<Order> order = userMono
    .flatMap(user -> orderRepository.findLatestByUserId(user.getId()));

변환 결과가 다시 MonoFlux일 때 씁니다. DB 조회나 API 호출처럼 비동기 작업을 체이닝할 때 필수예요. map()을 썼으면 Mono<Mono<Order>>가 되어버립니다.

zip — 여러 Mono/Flux 합치기

JAVA
Mono<Tuple2<User, List<Order>>> result = Mono.zip(
    userRepository.findById(userId),
    orderRepository.findByUserId(userId).collectList()
);

여러 비동기 작업을 ** 병렬로 실행 **하고 결과를 합칠 때 씁니다. 두 작업이 독립적이면 순차적으로 flatMap 체이닝하는 것보다 zip이 훨씬 효율적이에요.

onErrorResume — 에러 복구

JAVA
Mono<User> user = userRepository.findById(userId)
    .onErrorResume(ex -> {
        log.warn("DB 조회 실패, 캐시에서 조회", ex);
        return cacheRepository.findById(userId);
    });

에러가 발생했을 때 대체 Publisher로 전환해요. try-catch의 리액티브 버전이라고 보면 됩니다.

그 외 자주 나오는 연산자

연산자용도
filter()조건에 맞는 데이터만 통과
switchIfEmpty()Mono가 비어있을 때 대체값 제공
doOnNext()데이터가 지나갈 때 사이드 이펙트 (로깅 등)
collectList()Flux를 Mono<List<T>>로 변환
concatMap()flatMap과 비슷하지만 순서 보장
delayElements()각 요소 발행 사이에 지연 추가
retry(n)에러 시 n번 재시도

Spring WebFlux

개요

Spring WebFlux는 Spring 5에서 도입된 ** 논블로킹 리액티브 웹 프레임워크 **예요. Spring MVC와 나란히 존재하며, 둘 중 하나를 선택해서 쓰는 구조입니다.

PLAINTEXT
Spring Web (spring-web 모듈)
├── Spring MVC     ← 서블릿 기반, 블로킹
└── Spring WebFlux ← 리액티브 기반, 논블로킹

@RestController 방식 (어노테이션 기반)

Spring MVC 쓰던 사람에게 가장 친숙한 방식이에요. 겉보기엔 MVC 코드와 거의 비슷한데, 리턴 타입만 Mono/Flux로 바뀝니다.

JAVA
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<UserResponse>> getUser(@PathVariable Long id) {
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(UserResponse.from(user)))
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @GetMapping
    public Flux<UserResponse> getAllUsers() {
        return userService.findAll()
            .map(UserResponse::from);
    }

    @PostMapping
    public Mono<ResponseEntity<UserResponse>> createUser(
            @RequestBody Mono<UserRequest> request) {
        return request
            .flatMap(userService::create)
            .map(user -> ResponseEntity
                .created(URI.create("/api/users/" + user.getId()))
                .body(UserResponse.from(user)));
    }
}

Functional Endpoints (함수형 방식)

라우팅과 핸들러를 분리해서 함수형으로 정의하는 방식이에요. 어노테이션 없이 코드로 라우팅 규칙을 선언합니다.

JAVA
@Configuration
public class UserRouter {

    @Bean
    public RouterFunction<ServerResponse> userRoutes(UserHandler handler) {
        return RouterFunctions.route()
            .GET("/api/users/{id}", handler::getUser)
            .GET("/api/users", handler::getAllUsers)
            .POST("/api/users", handler::createUser)
            .build();
    }
}
JAVA
@Component
public class UserHandler {

    private final UserService userService;

    public UserHandler(UserService userService) {
        this.userService = userService;
    }

    public Mono<ServerResponse> getUser(ServerRequest request) {
        Long id = Long.valueOf(request.pathVariable("id"));
        return userService.findById(id)
            .flatMap(user -> ServerResponse.ok()
                .bodyValue(UserResponse.from(user)))
            .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> getAllUsers(ServerRequest request) {
        return ServerResponse.ok()
            .body(userService.findAll().map(UserResponse::from), UserResponse.class);
    }

    public Mono<ServerResponse> createUser(ServerRequest request) {
        return request.bodyToMono(UserRequest.class)
            .flatMap(userService::create)
            .flatMap(user -> ServerResponse
                .created(URI.create("/api/users/" + user.getId()))
                .bodyValue(UserResponse.from(user)));
    }
}

어떤 방식을 쓸지는 취향이에요. 실무에서는 어노테이션 방식이 압도적으로 많이 쓰입니다. MVC에서 넘어올 때 학습 비용이 적으니까요. 함수형은 복잡한 라우팅 조건이 필요하거나, 어노테이션을 쓰기 어려운 상황에서 유리해요.


Spring MVC vs WebFlux

두 모델의 차이를 비교표로 정리해두면 이해가 훨씬 명확해집니다.

구분Spring MVCSpring WebFlux
** 프로그래밍 모델**동기/블로킹비동기/논블로킹
** 기반 서버**Servlet Container (Tomcat)Netty (기본), Tomcat, Undertow
** 스레드 모델**요청당 1 스레드이벤트 루프 (소수 스레드)
** 동시성**스레드 풀 크기에 제한적은 스레드로 높은 동시성
** 리턴 타입**일반 객체, ResponseEntityMono<T>, Flux<T>
DB 접근JPA/JDBC (블로킹)R2DBC (논블로킹)
HTTP 클라이언트RestTemplate, RestClientWebClient
** 학습 곡선**낮음높음
** 디버깅**쉬움 (스택 트레이스 명확)어려움 (비동기 스택 트레이스)
** 생태계**방대함상대적으로 제한적

언제 WebFlux를 써야 하나

**WebFlux가 적합한 경우 **:

  • 대량의 동시 접속을 처리해야 하는 서비스 (채팅, 알림, 스트리밍)
  • I/O 바운드 작업이 많은 서비스 (API 게이트웨이, 마이크로서비스 간 통신)
  • SSE(Server-Sent Events)나 WebSocket 같은 스트리밍 응답이 필요한 경우
  • 적은 리소스로 높은 처리량이 필요한 경우

**MVC가 더 나은 경우 **:

  • CRUD 위주의 일반적인 웹 애플리케이션
  • JPA/Hibernate를 이미 쓰고 있는 경우 (블로킹 드라이버라서 WebFlux의 이점을 살리지 못함)
  • 팀원 대부분이 리액티브 프로그래밍에 익숙하지 않은 경우
  • CPU 바운드 작업이 주인 경우

솔직히 말하면, 대부분의 서비스에서는 Spring MVC로 충분하다. WebFlux는 "진짜 높은 동시성이 필요한 특정 서비스"에서 빛을 발한다. 무조건 WebFlux가 좋다는 식의 접근은 오히려 생산성을 깎아먹는다.


WebClient

RestTemplate은 왜 안 쓰나

RestTemplate은 블로킹 HTTP 클라이언트예요. 요청 보내고 응답 올 때까지 스레드가 멈춰 있습니다. Spring 5부터 maintenance 모드로 들어갔고, 공식 문서에서도 WebClient를 쓰라고 권고하고 있어요.

참고로 Spring 6.1부터는 동기 환경에서 쓸 수 있는 RestClient도 나왔습니다. 블로킹 환경이면 RestClient, 논블로킹 환경이면 WebClient를 쓰면 돼요.

WebClient 사용법

JAVA
@Configuration
public class WebClientConfig {

    @Bean
    public WebClient webClient() {
        return WebClient.builder()
            .baseUrl("https://api.example.com")
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();
    }
}
JAVA
@Service
public class ExternalApiService {

    private final WebClient webClient;

    public ExternalApiService(WebClient webClient) {
        this.webClient = webClient;
    }

    // GET 요청
    public Mono<Product> getProduct(Long id) {
        return webClient.get()
            .uri("/products/{id}", id)
            .retrieve()
            .bodyToMono(Product.class);
    }

    // POST 요청
    public Mono<Product> createProduct(ProductRequest request) {
        return webClient.post()
            .uri("/products")
            .bodyValue(request)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, response ->
                response.bodyToMono(String.class)
                    .flatMap(body -> Mono.error(
                        new ClientException("요청 오류: " + body))))
            .bodyToMono(Product.class);
    }

    // 여러 API 병렬 호출
    public Mono<OrderDetail> getOrderDetail(Long orderId) {
        Mono<Order> orderMono = webClient.get()
            .uri("/orders/{id}", orderId)
            .retrieve()
            .bodyToMono(Order.class);

        Mono<User> userMono = webClient.get()
            .uri("/users/{id}", orderId)
            .retrieve()
            .bodyToMono(User.class);

        return Mono.zip(orderMono, userMono)
            .map(tuple -> new OrderDetail(tuple.getT1(), tuple.getT2()));
    }
}

WebClient의 진짜 장점은 병렬 호출이에요. 위의 getOrderDetail() 예시에서 Mono.zip()을 쓰면 두 API를 동시에 호출하고 둘 다 완료되면 합칩니다. RestTemplate이었으면 순차적으로 호출해서 총 대기 시간이 두 API 응답 시간의 합이었을 거예요.


R2DBC — 리액티브 DB 접근

JPA를 WebFlux에서 못 쓰는 이유

JPA는 내부적으로 JDBC를 씁니다. JDBC는 태생이 블로킹이에요. DB에 쿼리 날리고 결과 올 때까지 스레드가 블로킹됩니다. WebFlux 환경에서 JPA를 쓰면 논블로킹의 이점이 전부 날아가요. 이벤트 루프 스레드가 블로킹되면 전체 시스템이 멈출 수 있습니다.

R2DBC란

R2DBC(Reactive Relational Database Connectivity) 는 관계형 데이터베이스를 논블로킹으로 접근하기 위한 스펙이에요. JDBC의 리액티브 버전이라고 생각하면 됩니다.

JAVA
public interface UserRepository extends ReactiveCrudRepository<User, Long> {

    Flux<User> findByNameContaining(String name);

    @Query("SELECT * FROM users WHERE email = :email")
    Mono<User> findByEmail(String email);
}

Spring Data R2DBC를 쓰면 ReactiveCrudRepository를 상속받아서 JPA의 JpaRepository와 비슷한 느낌으로 사용할 수 있어요. 다만 몇 가지 차이점이 있습니다.

구분JPA + JDBCSpring Data R2DBC
I/O 모델블로킹논블로킹
** 리턴 타입**User, List<User>Mono<User>, Flux<User>
** 영속성 컨텍스트**있음 (1차 캐시, 변경 감지, 지연 로딩)없음
** 지연 로딩**지원미지원
** 연관관계 매핑**@OneToMany, @ManyToOne직접 조인 쿼리 작성
** 트랜잭션**@Transactional (ThreadLocal 기반)@Transactional (Reactor Context 기반)

R2DBC는 JPA 같은 ORM이 아니에요. 영속성 컨텍스트도 없고, 지연 로딩도 없고, 연관관계 매핑도 안 됩니다. 단순한 리액티브 DB 클라이언트에 가깝다고 보면 돼요. 복잡한 도메인 모델이라면 솔직히 JPA가 훨씬 편하고, R2DBC는 단순한 CRUD나 읽기 위주 서비스에 적합합니다.


Netty — WebFlux의 기본 서버

Tomcat vs Netty

Spring MVC는 Tomcat 위에서 돌아가지만, WebFlux는 기본으로 Netty 를 사용해요.

Netty는 이벤트 루프 기반의 비동기 네트워크 프레임워크입니다. Node.js의 이벤트 루프와 개념이 같아요.

PLAINTEXT
[이벤트 루프 (소수의 스레드)]
  ├── 요청 1 수신 → 핸들러 실행 → I/O 대기 (스레드 반납) → 완료 콜백
  ├── 요청 2 수신 → 핸들러 실행 → I/O 대기 (스레드 반납) → 완료 콜백
  ├── 요청 3 수신 → ...
  └── ...수만 개 커넥션 처리 가능
구분TomcatNetty
** 모델**스레드 풀 (요청당 스레드)이벤트 루프 (Boss/Worker)
** 기본 스레드 수**200CPU 코어 수 × 2
I/O 처리블로킹논블로킹 (NIO)
** 용도**Servlet 기반 웹 앱고성능 네트워크 서버

Netty의 이벤트 루프 구조는 이래요.

  • Boss Group: 새로운 커넥션을 accept하는 스레드 그룹
  • Worker Group: 실제 I/O를 처리하는 스레드 그룹 (이벤트 루프)
  • 각 Worker 스레드는 여러 채널(커넥션)을 담당하며, I/O 이벤트가 발생하면 핸들러 파이프라인을 통해 처리

** 이벤트 루프 스레드를 절대 블로킹하면 안 됩니다.** Thread.sleep(), 블로킹 I/O, synchronized 블록 같은 걸 이벤트 루프에서 쓰면 해당 스레드가 담당하는 모든 커넥션이 멈춰요. 이게 WebFlux에서 가장 조심해야 할 부분이에요.

만약 어쩔 수 없이 블로킹 코드를 써야 한다면 Schedulers.boundedElastic()으로 별도 스레드에서 실행해야 합니다.

JAVA
Mono<String> result = Mono.fromCallable(() -> blockingOperation())
    .subscribeOn(Schedulers.boundedElastic());

주의할 점

리액티브가 항상 빠른가?

아닙니다. CPU 바운드 작업에서는 리액티브가 더 느릴 수 있어요. 리액티브의 장점은 I/O 대기 시간 동안 스레드를 다른 작업에 쓸 수 있다는 건데, CPU를 계속 쓰는 연산에서는 그런 대기 시간이 없습니다. 오히려 리액티브 체인의 오버헤드(스케줄링, 컨텍스트 스위칭)만 추가돼요.

간단한 CRUD 앱에서 벤치마크를 돌려보면 MVC와 WebFlux의 성능 차이가 거의 없거나 MVC가 더 빠른 경우도 있습니다. WebFlux가 빛나는 건 ** 동시 접속이 수천~수만이면서 I/O 대기가 많은 상황 **이에요.

디버깅은 어떻게 하나?

리액티브 코드의 가장 큰 고통은 디버깅이에요. 비동기 체인에서 에러가 터지면 스택 트레이스가 의미 없는 내부 코드로 가득 차서 원인을 찾기 어렵습니다.

몇 가지 팁이 있어요.

  • checkpoint("설명"): 체인 중간에 넣으면 에러 발생 시 해당 지점 정보를 스택 트레이스에 추가해줍니다.
  • log(): 체인의 신호(onNext, onComplete, onError, request)를 전부 로깅해요.
  • Hooks.onOperatorDebug(): 전역적으로 모든 연산자에 어셈블리 스택 트레이스를 추가합니다. 성능 비용이 크니까 프로덕션에서는 쓰면 안 돼요.
  • ReactorDebugAgent: 바이트코드 변환 방식으로 Hooks.onOperatorDebug()보다 성능 부담이 적습니다.
JAVA
userRepository.findById(userId)
    .checkpoint("사용자 조회 후")
    .flatMap(user -> orderService.findByUserId(user.getId()))
    .checkpoint("주문 조회 후")
    .subscribe();

가상 스레드(Project Loom)가 WebFlux를 대체하나?

Java 21에서 정식으로 도입된 가상 스레드(Virtual Thread)는 경량 스레드예요. OS 스레드가 아니라 JVM이 관리하는 스레드라서, 수십만 개를 만들어도 메모리 부담이 적습니다. 블로킹 I/O를 해도 OS 스레드가 블로킹되지 않아요.

그러면 WebFlux가 필요 없어지는 거 아닌가 싶을 수 있는데, ** 완전히 대체하지는 않습니다.**

  • 가상 스레드는 ** 동기 코드를 그대로 쓰면서도 높은 동시성 **을 달성할 수 있게 해줘요. 기존 Spring MVC + JPA 코드를 바꾸지 않아도 됩니다.
  • WebFlux는 ** 스트리밍, 백프레셔, 리액티브 합성(composition)** 같은 기능을 추가로 제공해요. SSE나 WebSocket 스트리밍 같은 유스케이스에서는 여전히 WebFlux가 자연스럽습니다.
  • 가상 스레드는 "요청-응답" 패턴에서 블로킹 코드의 확장성 문제를 해결하고, WebFlux는 "데이터 스트림" 패턴에서 빛나요.

** 한 줄 정리 **: 가상 스레드는 블로킹 I/O의 확장성 문제를 해결해서 Spring MVC의 동시성을 크게 높여주지만, 스트리밍이나 백프레셔처럼 리액티브 프로그래밍 자체의 장점이 필요한 경우에는 WebFlux가 여전히 유효하다.


파생 개념

개념연결
Spring MVCDispatcherServlet, 서블릿 기반 요청 처리 흐름 — 이미 정리함
Java 동시성synchronized, volatile, ExecutorService, CompletableFuture — 이미 정리함
이벤트 루프Node.js의 싱글 스레드 이벤트 루프와 같은 개념. Netty는 멀티 스레드 이벤트 루프
Reactive Streams 심화Hot vs Cold Publisher, Sinks, 스케줄러 — 리액티브 시리즈에서 다룸
댓글 로딩 중...