▶ 개요 및 배경
별 헤는 밤 버전 업데이트를 진행하면서, 날씨 페이지를 맡게 되었다.
날씨 페이지의 메인 로직은 외부 api를 호출하고, 응답받은 날씨 데이터를 적절히 분석하여 보여주는 것이다.
날씨 페이지에서는 아래와 같은 2가지 외부 api 호출이 필요하다.
- openWeather : 분, 시간, 일 단위로 날씨 예보 데이터를 제공하는 api (https://openweathermap.org/)
- 에어코리아 : 시도별 미세먼지 데이터를 제공하는 api (https://www.data.go.kr/data/15073861/openapi.do)
기존에는 앱단에서 직접 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를 쓰는 것을 권장하고 있으니, 이런 개념에 대해 한번쯤 알아두면 좋을 것 같다.
<별 헤는 밤> 구경가기
'별밤 일지 > 개발' 카테고리의 다른 글
[Android] CustomView 를 활용한 효율적인 나만의 View 만들기 (0) | 2023.12.13 |
---|---|
[JPA] 검색 메소드 수정하기 - JPA Specification (0) | 2023.12.07 |
[Android] Viewpager2, Scrollview 터치와 스크롤 분리하기 (0) | 2023.11.29 |
[JPA] 검색 메소드 수정하기 - N+1 문제 (2) | 2023.10.24 |
[Android] 게시글 댓글 기능 구현하기 (0) | 2023.10.18 |