따라서외부 API가 호출되는 시간 자체를 단축할 수 있는 방법을 찾다가 WebClient를 만나게 되었다.
(WebClient외에도 여러개의 외부 API 호출을 비동기로 처리할 수 있는 CompletableFuture(자바 8이상)이 있으며 asyncRestTemplate도 있다.)
2. WebClient?
일단 기존에 사용하던RestTemplate은 Spring 5.0부터는 유지보수모드라며 5.0부터는 WebClient 사용을 권장하고 있다.
NOTE: As of5.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를 자세하게 학습하기 위해 이 글을 클릭하셨다면 하단의 공식 메뉴얼을 추천드립니다!)
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는 스케줄러에 의해 비동기적으로 수행된다.