Backend/Spring+Boot

[SpringBoot] SpringMVC 에서 WebClient 사용시 주의사항

findmypiece 2022. 7. 28. 02:25
728x90

WebClient 는 Spring 에서 제공하는 RestClient 의 한 종류이다. 과거에 사용되던 RestTemplate 과 비슷한 역할이라고 생각하면 되는데 RestTemplate 는 장기적으로 Deprecated 예정이기 때문에 이제는 WebClient 를 사용해야 한다.

WebClient 의 가장 큰 특징은 RestTemplate 과 다르게 논블로킹을 지원한다는 것이다. 말만 들으면 성능에 굉장한 이득이 있을 것 같지만 논블로킹 방식으로 사용하지 않으면 RestTemplate 을 사용하는 것과 차이가 없다.

WebClient 의 응답값은 Mono 또는 Flux 타입이다. Mono, Flux 는 리액티브 스트림을 구성하는 파이프라인의 일부로 웹플럭스에서는 기본적으로 논블로킹 처리되지만 SpringMVC 환경에서 사용시에는 논블로킹 처리를 수동으로 구성해줘야 한다.

우선 Mono, Flux는 결과를 미래에 알 수 있다는 점에서 Future와 비슷하지만 Future 는 이미 시작되었음을 의미하지만 Mono, Flux 는 시작할 수 있음을 의미한다는 점을 인지하고 있어야 한다.

즉, WebClient의 호출을 단순히 Mono, Flux 을 리턴하도록 하고 끝낸다면 대상 서버를 호출조차 하지 않는다는 말이다.

Mono<Integer> mono = webClient.get().uri("/test1")
                .retrieve()
                .bodyToMono(Integer.class);
                                
Flux<Integer> flux = webClient.get().uri("/test2")
                .retrieve()
                .bodyToFlux(Integer.class);


이걸 어떻게 처리할지 추가 정의해야 작업이 시작 되는데 블로킹방식과 논블로킹 방식으로 처리할 수 있고 블로킹 방식은 아래와 같이 사용하면 된다.

int result = webClient.get().uri("/test1")
                .retrieve()
                .bodyToMono(Integer.class)
                .block();
                
List<Integer> resultList = webClient.get().uri("/test2")
                .retrieve()
                .bodyToFlux(Integer.class)
                .toStream()
                .collect(Collectors.toList());


문제는 이는 블로킹 방식으로 RestTemplate 를 사용할 때와 성능상 차이가 없고 성능 향상을 위해서는 논블로킹 방식으로 사용할 필요가 있다.

논블로킹 방식으로 사용하는 방법은 아래와 같다.

AtomicInteger result = new AtomicInteger();
List<Integer> resultList = new ArrayList<>();

webClient.get().uri("/test1")
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(e -> result.set(e));

webClient.get().uri("/test2")
                .retrieve()
                .bodyToFlux(String.class)
                .subscribe(e -> resultList.add(e));


위 코드에서 2번의 WebClient 호출은 논블로킹으로 동작한다. 이에 메인스레드가 종료되면 완료여부와 무관하게 곧바로 종료된다.

해서 메인스레드 입장에서 각 호출의 처리완료를 기다려야 하고 일반적으로 사용되는 방식은 아래와 같이 CountDownLatch 를 이용한 방법이다.

CountDownLatch cdl = new CountDownLatch(2);

AtomicInteger result = new AtomicInteger();
List<Integer> resultList = new ArrayList<>();

webClient.get().uri("/test1")
                .retrieve()
                .bodyToMono(String.class)
                .doOnTerminate(() -> cdl.countDown())
                .subscribe(e -> result.set(e));

webClient.get().uri("/test2")
                .retrieve()
                .bodyToFlux(String.class)
                .doOnTerminate(() -> cdl.countDown())
                .subscribe(e -> resultList.add(e));
                
cdl.await();


어차피 처리결과를 기다려야 한다면 블로킹이 발생하게 되는데 무엇이 이득이냐고 생각될 수 있지만 메인스레드 입장에서는 블로킹이지만 각각의 WebClient 호출은 논블로킹으로 호출된다.

이에 각각의 호출이 5초가 소요된다고 가정하면 WebClient 를 블로킹 방식으로 사용한다면 총 처리시간은 10초가 될테지만 위와 같이 논블로킹 방식으로 사용한다면 총 처리시간은 5초가 된다.

참고로 WebClient를 코루틴에서 사용할 경우 Mono와 Flux 를 그대로 사용하진 못하고 변환을 해야 하는데 각각 아래와 같이 대체하면 된다.
bodyToMono == awaitBody
bodyToFlux == bodyToFlow

코루틴에서 역시 메인스레드에서 처리의 완료를 기다려야 하는데 runBlocking 블록을 사용하면 되고 launch, async 블록들을 활용해서 논블로킹 처리가 가능하다.

https://stackoverflow.com/questions/59607165/what-is-the-difference-between-block-subscribe-and-subscribe
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
https://doinge-coding.tistory.com/entry/spring-webClient사용방법
https://medium.com/@odysseymoon/spring-webclient-사용법-5f92d295edc0
https://happycloud-lee.tistory.com/220
https://godekdls.github.io/Reactive%20Spring/webclient/
https://umbum.dev/1114
https://m.blog.naver.com/spdlqjdudghl/221816497872
https://stackoverflow.com/questions/45418523/spring-5-webclient-using-ssl
https://docs.spring.io/spring-boot/docs/2.1.18.RELEASE/reference/html/boot-features-webclient.html
https://velog.io/@jaepani5015/WebClient
728x90