관리 메뉴

개발노트

[Spring Webclient] Webclient의 non-blocking 특성 살리기. 본문

Framework/Springboot

[Spring Webclient] Webclient의 non-blocking 특성 살리기.

YoonGwon 2022. 10. 12. 01:45

1. WebClient를 사용 배경

1) 프로젝트에서의 문제점

프로젝트를 진행하던 중, 하나의 클라이언트 요청에 대해 여러 번의 외부 API를 호출(최대 16번....)해야하는 상황이 생겼다.

RestTemplate을 사용하여 모든 외부 API를 동기적으로 호출하다보니 평균적으로 5초이상 걸렸으며 10초가 넘는 경우도 발생하였다.

죽여죠.....

2) 해결하기 위한 시행착오

처음에 생각한 방식은 병렬처리 방식이다.

각각의 음식점에 대해 url, 이미지, 카테고리를 각각 외부 API를 호출하여 가져오고 있으므로 음식점으로 스트림을 생성하여 3개의 작업(외부 API를 호출하는 작업)을 병렬적으로 처리하면 된다고 생각했다.

 

결과는 시간이 매우 단축되고 아주 좋았다! .....로컬에서만 말이다^^ㅠ

로컬에서 개발하던 나의 PC는 8코어였지만 배포 서버는 달랑 1코어(AWS 프리티어의 t2.micro)였다.

싱글 코어 CPU일 경우에는 순차 처리가 빠르다. 병렬 처리를 할 경우 스레드의 수만 증가하고 번갈아 가면서 스케쥴링을 해야하므로 좋지 못한 결과를 준다. 코어의 수가 많으면 많을 수록 병렬 작업 처리 속도는 빨라진다.

출처: https://ict-nroo.tistory.com/43 [개발자의 기록습관]

 

따라서 외부 API가 호출되는 시간 자체를 단축할 수 있는 방법을 찾다가 WebClient를 만나게 되었다.

(WebClient외에도 여러개의 외부 API 호출을 비동기로 처리할 수 있는 CompletableFuture(자바 8이상)이 있으며 asyncRestTemplate도 있다.)

 

2. WebClient?

일단 기존에 사용하던 RestTemplate은 Spring 5.0부터는 유지보수모드라며 5.0부터는 WebClient 사용을 권장하고 있다.

NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

그렇다면 WebClient은 무엇일까? Spring Framework 공식문서에서는 다음과 같이 소개한다.

  • Http 요청을 수행하는 non-blocking, reactive 클라이언트
  • Spring 5.0부터 지원
  • 인터페이스이므로 create(), create(String), builder()를 사용해 객체를 생성

이해를 돕기 위해 기존에 사용한 RestTemplate의 특징과도 비교해보자.

  • Http 요청을 수행하는 동기식 클라이언트
  • Spring 3.0부터 지원
  • 기본 HTTP 라이브러리(HttpURL Connection, Apache Http Component) 사용
  • Restful 형식 사용

결국 RestTemplate과 가장 크게 비교되는 점은 non-blocking 방식인데, 이는 호출한 시스템의 처리를 기다리지 않고 다음 처리로 넘어갈 수 있음을 의미한다. 따라서 동시에 다른 작업이 가능하여 속도 향상이 가능하다.

(동기와 비동기, blocking과 non-blocking 차이: https://musma.github.io/2019/04/17/blocking-and-synchronous.html )

 

3. 적용 방법

그럼 이렇게나 권장하는 WebClient를 프로젝트에 적용시켜보자!

(개인 프로젝트에 적용한 방식을 기록했을 뿐이라서 WebClient를 자세하게 학습하기 위해 이 글을 클릭하셨다면 하단의 공식 메뉴얼을 추천드립니다!)

 

1) Configuration

스프링부트 공식메뉴얼에서는 각 컴포넌트에서 WebClient를 주입받아 객체를 생성하는 것을 권장한다.

하지만 나는 개인 프로젝트 + 서비스 클래스 코드 길이가 너무 길어져서 @Configuration과 @Bean으로 Spring Bean을 생성하여 사용하였다.

create()를 통해 connection, read, write timeout을 미리 설정하여 HttpClient를 구성하고 WebClient를 생성하였다.

 

외부 API를 호출할 때는, mutate()함수로 생성한 WebClient를 복제하여 새로운 WebClient를 생성해 필요한 옵션을 추가하였다.

다음은 WebClient를 사용하여 구글 Place Api에 음식점 세부정보를 GET 방식으로 요청하는 코드이다.

retrieve(): response body를 가져와 decode한다. toEntity(), bodyToMono(), bodyToFlux() 함수를 사용하여 데이터에서 원하는 정보의 객체를 추출할 수 있다.

(retrieve() 함수를 사용하는 대신 Exchange 방식도 있다. response status에 따라 response를 다르게 디코딩하는 등 더욱 많은 제어가 필요할때 유용하다.)

Mono: Reactive Streams 인터페이스 중 Publisher의 구현체이다. 0 ~ 1개의 데이터를 전달한다.

 

그동안 MVC 패턴만 접한 나는 Reactive Stream, Mono와 같은 처음보는 단어들이 당황스러웠다.

거의 이틀내내 Reactive에 대해서만 찾아본듯하다..

간단하게 설명하자면 Reactive Stream은 Reactive Programming에 대한 명세이고 인터페이스로 구성되어 있다. 따라서 Reactive Programming을 구현하기 위해서는 인터페이스를 구현해야 하는데 Mono가 그런 구현체의 일종이다. 생각해보면 그동안 MVC 패턴에서 동기적으로만 데이터를 처리하였는데 비동기적인 데이터를 처리하기 위한 새로운 객체를 써야하는게 자연스럽다.

 

 

2) 로직 개선과 적용

기존의 서비스 로직은 다음과 같은 흐름이였다. (데이터베이스에 접근하는 로직은 여기선 제외)

하나의 음식점의 세부정보를 위한 API 호출
1. 외부 API 호출을 통해 음식점 리스트를 가져온다.
2. 상위 5개의 데이터에 대해 세부정보(카테고리, 음식점 사진, 음식점 url)가 없는 경우 해당 정보를 추가한다.
  1) 카테고리 정보가 없는 경우, 외부 API를 호출하여 카테고리 정보를 추가한다.
  2) 음식점 사진 정보가 없는 경우, 외부 API를 호출하여 음식점 사진 정보를 추가한다.
  3) 음식점 url 정보가 없는 경우, 외부 API를 호출하여 음식점 url 정보를 추가한다.
3. 클라이언트에게 음식점 리스트를 반환한다.

non-blocking을 사용하여 개선한 방식이다.

하나의 음식점의 세부정보를 위한 API 호출
1. 외부 API 호출을 통해 음식점 리스트를 가져온다. (동기)
2. 상위 5개의 데이터에 대해 세부정보(카테고리, 음식점 사진, 음식점 url)가 없는 경우 해당 정보를 추가한다.
  1) 카테고리 정보가 없는 경우, 비동기으로 외부 API를 호출하여 카테고리 정보를 추가한다.
  2) 음식점 사진 정보가 없는 경우, 비동기적으로 외부 API를 호출하여 음식점 사진 정보를 추가한다.
  3) 음식점 url 정보가 없는 경우, 비동기적으로 외부 API를 호출하여 음식점 url 정보를 추가한다.
3. 클라이언트에게 음식점 리스트를 반환한다.

non-blocking으로 처리한 부분을 코드로 간단하게 나타내면 다음과 같다. (변형한 전체 코드)

사용한 함수를 간단하게 소개하자면 다음과 같다.

 

subscribeOn(): subscribe을 수행할 때 실행할 별도의 스케줄러를 지정한다. 공식 문서에서 I/O blocking 작업에 추천하는 boundedElastic() 스케줄러를 사용하였다. 해당 스케줄러는 결과를 기다리는 전용 스레드를 생성하면서 다른 non-blocking 작업에는 영향을 주지 않는다.

zip(): 타입이 다른 2개이상의 Mono의 결과를 묶어서 처리한다.

block(): 결과를 동기적으로 처리한다.

 

즉, zip을 통해 최종적인 결과는 동기적으로 처리되지만, 외부 API를 호출하는 각각의 Mono는 스케줄러에 의해 비동기적으로 수행된다.

 

4. 결과

DB까지 날린 후, 같은 조건에서 포스트맨으로 API호출을 비교한 결과이다.

before

after

 

*해당글은 https://suyeonchoi.tistory.com/61  님의 글을 인용하였습니다. 

 

 

참고자료)

SpringFramework 공식문서

WebClient 가이드

Reactor Schedulers: https://projectreactor.io/docs/core/release/api/reactor/core/scheduler/Schedulers.html

[Baeldung]Simultaneous WebClient Calls : https://www.baeldung.com/spring-webclient-simultaneous-calls

 

기업 기술 블로그

개인블로그

 

자바의 동시성 #1 - 물리적 아키텍쳐와 자바 스레드

자바의 동시성 #1 - 물리적 아키텍쳐와 자바 스레드 Jan 14, 2019 독자들이 필자의 Effective Java 3rd의 동시성에 관한 포스팅을 읽거나 동시성에 대해 공부할 때, 동시성에 관한 기본 개념을 알아두면

badcandy.github.io

 

[전체글 참조] : https://suyeonchoi.tistory.com/61

728x90