본문 바로가기
별밤 일지/개발

[WebClient] 비동기 아키텍처를 통한 외부 api 콜 성능 개선

by 별밤 에디터 2023. 11. 4.

▶ 개요 및 배경

별 헤는 밤 버전 업데이트를 진행하면서, 날씨 페이지를 맡게 되었다.

날씨 페이지의 메인 로직은 외부 api를 호출하고, 응답받은 날씨 데이터를 적절히 분석하여 보여주는 것이다.

날씨 페이지에서는 아래와 같은 2가지 외부 api 호출이 필요하다.

기존에는 앱단에서 직접 api를 호출하는 방식이었는데, 해당 구조는 다음과 같은 문제점이 있었다.

  • 프런트 단에서 데이터를 직접 호출하는 것이므로 성격에 맞지 않는다고 판단
  • 날씨 데이터를 파싱하고 분석하는 로직이 복잡하여 코드가 길어지고 가독성이 떨어짐
  • 기획이 수정되면서 호출한 날씨 정보를 DB에 저장해야 되는 기능이 필요해짐

이에 따라 외부 api 호출을 BE 단에서 하는 구조로 수정하였고, 성능 개선을 고민하게 되면서 외부 api를 호출할 때 WebClient라는 http 호출 클라이언트 라이브러리를 사용했다.

 

▶ WebClient 란?

 

Spring 공식 문서에 다음과 같이 나와있다.

Spring WebFlux includes a client to perform HTTP requests with. WebClient has a functional, fluent API based on Reactor, see Reactive Libraries, which enables declarative composition of asynchronous logic without the need to deal with threads or concurrency.

 

… 역시 영어는 어렵다. 한줄 요약하면 다음과 같다.

WebClient = Spring WebFlux에 포함된 Http 호출 클라이언트로, Reactor를 기반으로 하여 비동기적으로 동작한다.

 

Http 호출 클라이언트라는 것은 알겠는데 Reactor 를 기반으로 하는 것은 무엇이며 왜 비동기로 동작한다는 것일까? Spring WebFlux 은 또 뭘까? 이에 대해 간단히 알아보자.

 

 

Spring WebFlux 🆚 Spring MVC

 

우리가 흔하게 사용하는 Spring MVC 패턴에서는 서버가 동시에 여러 요청을 받을 수 있도록 Multi Thread + Blocking

방식으로 동작한다.

 

위 그림과 같이 요청이 들어올 때마다 미리 만들어둔 Thread 를 할당하고, 만약 Thread Pool 이 꽉 차면 Queue에 대기 상태로 존재하게 된다.

이렇게 Spring MVC 패턴에서는 Thread 수를 늘려 동시성을 처리한다.

이와 달리 Spring WebFlux 는 하나의 Thread로 동작한다. Spring WebFlux 란, reactive programming을 할 수 있게 도와주는 스프링 모듈로, Single-Thread와 Non-Blocking 방식으로 동작한다.

 

위 그림과 같이 이벤트 루프를 사용해 응답이 올 때까지 Blocking 되지 않고 콜백 함수를 사용해 응답이 온 것을 알아 차린다. 따라서 적은 자원으로 여러 요청을 처리할 수 있다는 장점이 있다.

 

 

Reactor 란?

 

Reactor는 Reactive Programming 을 할 수 있게 만들어진, JVM 환경에서 동작하는 non-blocking reactive 라이브러리이다.

  • Reactive Programming

    리액티브 프로그래밍은 데이터 스트림 및 변경 전파와 관련된 선언적 프로그래밍 패러다임이며, 리액티브 프로그래밍을 사용하면 정적이나 동적인 데이터 스트림을 쉽게 표현할 수 있다. by https://en.wikipedia.org/wiki/Reactive_programming 

* 위에서 말한 데이터 스트림이란 데이터의 흐름이라고 이해하면 편하다.

 

Reactive Stream은 Publisher - Subscriber 구조로 되어있으며, 스트림에 데이터가 들어왔을 때 Publisher는 Subscriber에게 데이터를 Push 하게 된다. 여기서 중요한 점은 Subscriber가 Publisher를 구독하지 않으면 아무 일도 발생하지 않는다는 것이다. 이는 Publisher - Subscriber 구조의 매우 중요한 특징이므로 꼭 기억하길 바란다.

Reactor에서는 Mono와 Flux 라는 두 가지 reactive 데이터 타입이 제공된다.

Mono는 0 또는 1개의 항목을 가지는 리액티브 시퀀스를 나타내며, Flux는 여러 개 (0~N) 항목을 가지는 리액티브 시퀀스를 나타낸다.

 

Mono<Integer> data0 = Mono.just(1); // Mono 생성

Flux<Integer> data1= Flux.range(1, 10); // Flux 생성

 

위와 같이 간단하게 Mono, Flux를 생성할 수 있다. 하지만 위와 같이 코드를 작성하면 아무 일도 일어나지 않는다.

Subscriber가 Publisher를 구독하지 않으면 아무 일도 발생하지 않는다 라고 한 게 기억나는가?

 

Flux<Integer> data1= Flux.range(1, 10).log();

data1.subscribe(); // 이 때 체인이 실행됨

 

위와 같이 subscribe()를 붙여야 체인이 동작한다.

이 정도로 개념을 간단히 정리하고 날씨 페이지 코드를 살펴보자.

 

 

코드 구현

public Mono<OpenWeatherResponse> getOpenWeather(Double lat, Double lon) {

		UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(OPEN_WEATHER_URL);
		uriBuilder.queryParam("lat", lat);
		uriBuilder.queryParam("lon", lon);
		uriBuilder.queryParam("exclude", OPEN_WEATHER_EXCLUDE);
		uriBuilder.queryParam("appid", OPEN_WEATHER_API_KEY);
		uriBuilder.queryParam("units", OPEN_WEATHER_UNITS);
		
		return webClient.get()
		        .uri(uriBuilder.build().toUri())
		        .retrieve()
		        .toEntity(OpenWeatherResponse.class)
		        .flatMap(response -> {
		            log.info("HTTP Response for Open Weather ({}, {}) | {} | {}", lat, lon, response.getStatusCode(), response.getBody());
		            return Mono.just(Objects.requireNonNull(response.getBody()));
		        });
}

getOpenWeather 메서드

 

위 코드는 OpenWeather에서 날씨 예보 정보를 불러오는 코드이다.

webClient 뒤에 .get() 은 GET 요청을 의미하고,. uri() 은 말 그대로 요청 uri를 나타낸다.

. retrieve()는 응답을 받는 메서드로, ResponseEntity 형태로 받는다.

. toEntity()는 원하는 형태로 응답을 변환하는 메서드,. flatMap()는 Mono의 operator 중 하나로, 값을 변환하고 싶을 때 사용하는 메서드이다.

위에서 배운 Mono 형태로 데이터를 리턴하는 것을 알 수 있다.

 

public Map<String, String> getFineDustMap(String date) {

    UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(AIR_KOREA_URL);
    uriBuilder.queryParam("searchDate", date);
    uriBuilder.queryParam("serviceKey", AIR_KOREA_API_KEY);
    uriBuilder.queryParam("returnType", AIR_KOREA_RETURN_TYPE);
    uriBuilder.queryParam("informCode", AIR_KOREA_INFORM_CODE);

    Map<String, String> result = new HashMap<>();

    webClient.get()
            .uri(uriBuilder.build().toUri())
            .retrieve()
            .toEntity(AirKoreaResponse.class)
            .doOnNext(response -> {
                log.info("HTTP Response for Air Korea Get | {} | {}", response.getStatusCode(), response.getBody());
                AirKoreaResponse airKoreaResponse = response.getBody();

								// ...
								// airKoreaResponse 로 result 를 만드는 로직
								// ...
								
            })
            .retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(5)))
            .block();
    return result;
}

getFineDustMap 메서드

 

날씨 페이지에서는 날씨 예보 뿐만 아니라, 미세먼지 정보도 필요하다고 위에서 언급했다. getOpenWeather()와 비슷하게 webClient를 사용하여 외부 api를 호출하는 것을 알 수 있다.

다른 점은. block()를 사용한 것인데, 이 메서드는 비동기로 동작하는 webclient를 응답이 올 때까지 기다리게 하는 메서드이다. 사용하는 것을 권장하지 않지만, 비즈니스 로직 중 미세먼지 값을 받고 난 후, 처리해야 하는 로직이 있어 부득이하게 해당 함수를 사용하였다.

 

위에 설명한 WebFlux, Reactive, Mono, Flux 등에는 사실 매우 방대한 개념이 있고, 이 글만을 읽고는 이해하기 어려운 개념들이다. webClient 동작 방식, 사용법을 위해 간단히 설명한 정도이고 REAL 리액티브 프로그래밍을 위해선 application 전체를 Non-Blocking으로 구현해야 한다. 하지만 Spring에서 http 호출을 위해 webClient를 쓰는 것을 권장하고 있으니, 이런 개념에 대해 한번쯤 알아두면 좋을 것 같다.

 


<별 헤는 밤> 구경가기 

 

별 헤는 밤: 밤하늘, 별자리, 여행정보와 날씨예보까지 - Google Play 앱

오늘부터 별잘알! 오늘밤 별자리 정보, 날씨·광공해·월령까지 고려한 '관측적합도', 주변 관측지 검색으로 누구나 쉽게 밤하늘을 즐겨보세요.

play.google.com