카테고리 없음

WebFlux에서 Blocking을 처리하는 방법

alska95 2025. 2. 4. 14:25

 

WebFlux 프로그래밍에서 아래 Blocking 처리에 대한 사항만 잘 준수한다면 

WebFlux의 내부 구현이나 세부사항을 모르더라도 잘 돌아가는 애플리케이션을 만들 수 있다.


1. Blocking이란?

작업을 처리하는 스레드가 외부 리소스의 처리를 기다려야해서 작업이 중지된 채로 기다리고 있는 상태를 의미한다.

외부 리소스의 처리란 Network, Database I/O등을 의미한다.

파일 읽기, 쓰기, DB 조회나 외부 서비스 api를 호출 하는 경우 등이 해당된다.

객체의 변환이나 복잡한 계산 로직 처리는 현재 작업을 하고 있는 스레드가 할당 받은 자원으로 처리해야 하는 작업임으로 Blocking으로 봐서는 안된다.

 


2. WebFlux에서는 Blocking을 어떻게 비동기로 처리할까?

 

WebFlux의 Mono와 Flux를 사용한다고 해서 애플리케이션 로직이 비동기로 처리 되지는 않는다.

비동기 처리를 위해서는 Blocking요소가 발생했을 때 메인 스레드 대신 대기를 해줄 백그라운드(다른 스레드)가 필수적인데

Mono로 감싼다고 해서 새로운 스레드가 생겨나는게 아니기 때문이다.

Mono.fromCallable(() -> blockingOperation()) // 메인 스레드에서 블로킹 발생함
    .subscribe(result -> System.out.println("결과: " + result));

 

Blocking이 발생하는 Mono작업에 subscribeOn이나 publishOn을 사용하여 Scheduler을 등록해주면 적절한 스레드가 새로 할당 된다.

Blocking 작업과 이후 작업은 새로 할당된 스레드가 수행하게 된다.

이 때 Blocking작업은 사라지는게 아니며 새로 할당된 스레드에서 여전히 일어나게 되지만,

메인 스레드(이벤트 루프 스레드)는 Blocking없이 바로 다른 작업을 수행할 수 있게 된다.

Mono.fromCallable(() -> blockingOperation()) // 블로킹 작업
    .subscribeOn(Schedulers.boundedElastic()) // 별도 스레드에서 실행 - 메인 스레드 블로킹 되지 않음
    .subscribe(result -> System.out.println("결과: " + result));

 

 

cf)

Mono, Flux를 반환하기만 하면 애플리케이션 로직이 비동기로 처리 될 것이라는 오해가 종종 있다.

"Mono, Flux를 반환하면 Netty가 비동기로 클라이언트에게 응답을 한다." < 이것은 맞는 말이다.

다만 이것은 클라이언트 요청을 [수락하고, 읽고, 애플리케이션에서 작업을 처리하고 완성된 데이터를 쓸 때] Netty가 비동기로 수행한다는 의미이다.

우리가 짜는 코드는 "애플리케이션에서 작업을 처리"하는 코드이다.

"애플리케이션에서 작업을 처리"할 때 블로킹이 발생하는것은 Netty가 해결해줄 수 있는 부분이 아니고

우리가 코드를 짜서 Blocking을 우회해야하는 부분이다.

 

WebClient나 R2DBC를 사용할 때 사용하는 Mono와 Flux는 그냥 써도 비동기 처리가 되는게 맞다.

(일반적인 Mono, Flux와 구현체가 다르다)

WebClient의 경우 외부 api를 호출 할 때 즉시 스레드가 이벤트 루프로 반환된다.

후속 처리는 콜백 형태로 이벤트 key에 attach되고 IO대기는 OS에 위임한다.

(Netty 이벤트 루프에서 이벤트는 Key로서 관리되는데, Key에 데이터를 Attach해서 나중에 이벤트가 트리거 됐을 때 사용할 수 있다.)

그리고 IO가 완료되면 OS가 이벤트 루프에 이벤트를 다시 등록하고

이벤트 Key에 attach된 콜백이 실행된다.

WebClient, R2DBC에 대한 설명이 많이 빈약한데 이는 다른 포스트에서 보충하도록 하겠다.

 


3. WebFlux에서 Blocking을 비동기 처리를 하지 않으면 어떻게 될까?

 

Netty는 이벤트 루프 패턴을 구현하며 적은 수의 스레드를 사용하는데

적은 수의 스레드를 사용하면 Blocking(스레드가 외부 리소스를 기다리고 있는 상황)에 매우 취약하게 된다.

극단적인 예로 스레드가 1개인 Netty서버에서 Blocking이 발생한다면, 해당 이벤트 루프는 Blocking이 해소될 때 까지 아무것도 안하고 자원만 가지고 있는 상황이 발생한다.

서버 요청 처리에 매번 10초의 Blocking이 발생한다면, 이 웹 서버는 아무리 좋은 컴퓨팅 자원을 가지고 있더라도 10초에 1개의 요청만 처리할 수 있는 최악의 웹 서버가 될 것이다.

 


4. Blocking이 없는 요소를 Scheduler로 스레드를 분리해서 처리한다면 어떻게 될까?

스레드 변경을 최소화하는 것은 리액티브 프로그래밍의 기본 철학이기 때문에

정말 필요한 경우에만 스레드를 분리하는게 권장 되지만 큰 문제는 없다.

Blocking이 없더라도

병렬 처리가 필요하거나

시간이 너무 오래걸리는 cpu바운드한 작업은 이벤트 루프 스레드를 너무 오래 점유할 가능성이 있기 때문에

이러한 작업들을 Scheduler로 별도의 스레드로 분리하는것은 권장되는 패턴이다.

다만 매 처리마다 Scheduler로 스레드를 변경해야 한다면 기존 Tomcat을 사용하는것보다 못한 성능을 낼 수도 있다.

 


 

결론

WebFlux로직 처리에서는 이벤트 루프 스레드가 절대로 블로킹 되면 안된다.

그러니 

1. R2DBC, WEBCLIENT를 이용해 블로킹을 가능한한 최소화 해야 하며

2. [어쩔 수 없이 블로킹이 발생 하는 요소, 병렬 처리, 오래 걸리는 작업]은 Scheduler을 이용해 스레드를 분리 해야 하고

3. 불필요한 스레드 분리는 삼가해야한다.