[가자맵] Spring Web MVC에서 WebClient로 비동기 통신하기

김상인·2023년 8월 10일
0

가자맵

목록 보기
1/8
post-thumbnail

프로젝트 개발 중에 도로명 주소를 위경도로 변환하기 위해서 카카오 GeoAPI를 RestTemplate을 이용하여 처리하였다.
하지만 많으면 최대 1000번의 처리를 하는데 대략 30초 정도의 시간이 소요되었다.
사용자 입장에서는 응답을 기다리는데 답답할 것이다.

WebClient

시간을 줄이기 위해서 비동기 통신 기법이 필요했고 구글링을 하다가 WebClient를 알게되었다.
WebClient는 논블로킹으로 웹 요청을 보낼 수 있게 도와주는 라이브러리이다.
위 이미지는 논블로킹&비동기 방식이다.
어떤 프로세스에서 작업을 수행하다가 커널에 I/O요청을 해도 프로세스는 기다리지 않고 다음 작업을 수행할 수 있고 I/O에 대한 응답은 콜백으로 받을 수 있게 된다.

WebClient는 웹 요청을 블로킹, 논블로킹 모두 지원하지만 주로 비동기적이고 논블로킹으로 수행하는 인터페이스이다.

implementation("org.springframework.boot:spring-boot-starter-webflux")

WebClient를 사용하기 위해선 webflux를 의존받아야 한다.

WebFlux

Spring Framework 5.0에 도입된 새로운 반응형 웹 프레임워크로 Reactor API를 기반으로 동작한다.
Reactor는 JVM에서 논블로킹 애플리케이션을 구축하기 위한 Reactive Streams 사양을 기반으로 하는 라이브러리로 non-blocking backpressure로 비동기 스트림 처리를 위한 표준을 제공한다.
리액티브 프로그래밍에 관한 이미지다.
Publisher는 데이터를 생산하고 Subscriber는 생산된 데이터를 소비한다. 즉 데이터를 생산하고 Subscriber가 subscribe(구독)하면 생산된 데이터를 가지고 무엇을 할지 결정할 수 있다.

WebFlux는 어떻게 톰캣서버에서 멀티스레드로 돌아가는 Spring MVC에서 사용이 될까 의문이 들었다.
위 다이어그램처럼 Spring Framework 5.0부터 이 둘은 서로 상호작용하면서 사용할 수 있다.
그래서 꼭 Netty 서버가 아니더라도 Tomcat 위에서 동작할 수 있다.

그렇다면 Spring MVC에서 클라이언트의 요청이 들어오고 WebClient의 비동기 통신이 어떻게 처리가 되는지 과정을 알아보자.

동작 과정

클라이언트의 요청을 처리할 스레드들이 저장된 스레드풀이 있고 reactor-http-nio 라는 이름을 가진 작업 스레드가 EventLoop Group에 저장되어 있다.
reactor-http-nio는 비동기 작업을 처리하는 스레드로 보면된다.
이 작업 스레드는 직접 확인해 본 결과 CPU 코어 수 x2만큼 생성되는 걸로 보인다.

요청이 들어오면 스레드풀에서 스레드를 할당받고 Controller의 비즈니스 로직을 수행한다.

Controller에서 WebClient를 호출하면 EventLoop Group에서 작업스레드를 가져와서 별도로 WebClient의 작업을 수행한다. 그러면 스레드는 블로킹되지 않고 다른 작업을 계속해서 진행할 수 있다.

WebClient가 외부 API와 통신한다.
여기서 EventLoopTaskQueue가 보이는데 이것은 작업 스레드마다 별도로 생성되는 것으로 작업스레드가 처리할 작업들이 Queue 형태로 저장되어 하나씩 처리한다.

WebClient의 통신이 이뤄지는 동안 작업 스레드는 EventLoop Group에 반환된다.
그리고 외부API에서 응답을 받으면 콜백 이벤트가 일어나 EventLoopTaskQueue에 작업이 부여된다.

마지막으로 작업 스레드가 EventLoopTaskQueue에서 작업을 꺼내어 처리한다.
여기서 한 가지 생각해 볼 수 있는 점은 EventLoopTaskQueue에 긴 연산의 작업이 있을 경우 뒤에 있는 나머지 작업들은 오래 기다려야 한다는 점이다.

이제 WebClient로 개발하다가 발생한 이슈를 정리해보려고 한다.

발생한 이슈

429 예외

엑셀 파일을 통한 장소 등록 요청 시 등록할 장소 수만큼 카카오 geocode API를 요청하여 장소를 위경도로 변환하는데 429 Too Many Requests 문제 발생하였다.

알아보니 카카오 api가 분당 10만 건 씩만 처리할 수 있어서 이를 해결하기 위해 처리 속도를 늦추는 방법이 필요했다.
위 코드는 각 주소에 대해 위경도로 변환하기 위해 비동기 작업을 수행한다.
429 Too Many Request를 해결해주기 위해서
delayElements(DELAY_ELEMENTS_MILLIS) 에서 각 요소들 사이를 delay를 주었다.

하지만 Spring Web MVC는 멀티스레드로 사용자 요청을 처리한다.
즉 많은 사용자가 동시에 요청을 보내면 delay를 주어도 429 Too Many Request는 터지게 된다.

다음 포스팅에서 429 too many requests 해결하였다.

onErrorDropped

A, B, C 작업 순으로 처리하다가 가장 먼저 호출한 A작업에서 예외가 발생하면 전체 스트림이 terminated되고 나머지 B, C는 각각 onErrorDropped 발생하면서 printStackTrace()로 로그를 각각 출력하는데 중복되는 로그이기도 하고 너무 길기 때문에 로그 출력을 막고 싶었다.

	
Hooks.onErrorDropped { log.info("onErrorDropped.") }

Hooks는 Reactor 라이브러리에서 사용되는 전역적인 설정을 변경하거나 커스터마이징할 수 있는 유틸리티 클래스이다.
위 코드처럼 Hooks로 통해 전역으로 등록해주면 기존 onErrorDropped에 대한 처리를 재정의해서 처리할 수 있다.

나중에 해결해야 될 이슈

return mono.doOnSuccess(s -> {
            clientService.saveClientExcelData(groupId, clientExcelData);
        })

통신 작업이 오류 없이 모두 끝나고 결과를 DB에 저장해야 되는 상황이다.
문제는 DB에 저장하는 코드가 블로킹I/O 작업이라는 것이다.

어처피 비동기 작업이 다 끝났으니깐 블로킹I/O 작업을 해도 되는 거 아니야?
라고 생각할 수 있지만 후처리 작업을 실행시키는 스레드는 reactor-http-nio 이다.
만약 reactor-http-nio의 EventLoopTaskQueue에 해야 될 작업이 남은 상태에서 시간 지연이 되는 블로킹I/O작업을 하게 된다면 작업들은 모두 대기하고 있으며 비동기 통신 작업 또한 못하게 된다.

아직 해결한 건 아니지만 찾아본 바로는 2가지가 있다.
첫 번째는 R2DBC (Reactive Relational Database Connectivity)를 사용해서 DB작업도 리액티브 하게 하는 것이다. 문제는 이걸로 할 경우 현재 DB관련한 모든 코드들이 다 변경돼야 하기 때문에 당장 사용하지는 못한다.

두 번째는 작업을 다른 스레드로 전환하는 것이다.
Spring WebFlux는 데이터 흐름 체인 사이에서 처리를 다른 스레드 풀로 전환하는 메커니즘을 제공한다고 한다.

Scheduler scheduler = Schedulers.newBoundedElastic(5, 10, "MyThreadGroup");

WebClient.create("http://localhost:8080/index").get()
  .retrieve()
  .bodyToMono(String.class)
  .publishOn(scheduler)
  .doOnNext(s -> printThreads());

이런 식으로 스케줄러를 생성해서 처리해야 되는 작업에 대해 스레드를 전환해서 해결할 수 있다고 하는데 좀 더 알아봐야 될 것 같다...,

참조

https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html
https://happycloud-lee.tistory.com/220
https://www.baeldung.com/spring-5-webclient
https://www.stefankreidel.io/blog/spring-webmvc-with-webclient
https://www.baeldung.com/spring-webflux-concurrency
https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html#flatMap-java.util.function.Function-int-
https://tacogrammer.com/onerrordropped-explained/

profile
백엔드 희망자

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

많은 도움이 되었습니다, 감사합니다.

답글 달기