Spring 에서 외부 API를 요청할 때,
사용하는 라이브러리들은 RestTemplate, WebClient, OpenFeign 이 있다.
이번에는 비동기 방식을 지원하는 WebClient에 대해 알아보자.
1. WebClient 란?
WebClient는 Spring WebFlux에 포함되어 있는 HTTP 요청 라이브러리로, Reactor 기반의 API 를 가지고있다.
반응형 라이브러리를 참조하면 스레드나 동시성을 처리할 필요 없이
비동기 로직의 선언적 구성을 가능하게 하고 완전한 논 블로킹으로 구현되어있다.
물론 동기 방식으로도 사용이 가능하기에 사용자의 구현에 따라 동기/비동기 선택이 가능하다.
2. 구성
WebClient를 생성하는 방법은,
- WebClient.create()
- WebClient.builder().build()
이 두가지 방법을 제공하고 있다.
Builder() 방법을 사용하면 추가적인 옵션 설정을 아래와 같이 제공한다.
- uriBuilderFactory : 기본 URL로 사용할 uriBuilderFactory
- defaultUriVariables : URI 템플릿을 확장할때 사용할 기본 값
- defaultHeader : 모든 요청에 대한 헤더 설정
- defaultCookie : 모든 요청에 대한 쿠키 설정
- defaultRequest : 소비자가 모든 요청을 커스텀
- filter : 모든 요청에 대한 클라이언트 필터 설정
- exchangeStrategies : HTTP 메시지 Read/Write 커스텀
- clientConnector : HTTP 클라이언트 라이브러리 설정
- observationRegistry : Observability 지원을 활성화하는 데 사용할 레지스트리
- observationConvention : 기록된 관찰에 대한 메타데이터를 추출하는 선택적인 사용자 정의 규칙
a. 설정 예시
// * Builder
WebClient client = WebClient.builder()
.codecs(configurer -> ... )
.build();
// * Fiter
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
// * MaxInMemorySize
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
// * TimeOut
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
3. 요청/응답
요청에 대한 응답값을 어떤식으로 받을지에 대한 설정을
retrieve() 와 exchage() 방식 두가지를 사용한다.
a. retrieve() vs exchange()
retrieve() : ClientResponse 를 해독(decode)하고 사용자가 사용할 수 있도록 미리 만들어진 개체를 전달
exchage() : 응답 상태 및 헤더와 함께 ClientResponse Object 자체를 전달, 응답 개체를 세밀하게 제어할 수 있고 응답 개체와 예외를 더 효과적으로 처리할 수 있다.
//: retrieve 예시
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
//: exchange 예시
Mono<Person> mono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
설명 상으로는 'exchage() 를 사용하는 것이 더 좋지 않을까?' 하는 생각이 들 수 있지만,
아래 각 메소드에 대한 설명을 캡쳐한 이미지에서 볼수 있듯이,
exchange() 의 경우,
응답에 대한 모든 내용(response contents)를 처리하지 못하면 메모리 누수가 발생할 수 있기 때문에, retrieve() 를 권장한다고 되어있다.
exchange() 메소드가 Deprecate 되어있긴 하지만 사용은 가능하고,
retrieve() 보다 더 많은 Contents 에 대한 처리가 필요하다면,
exchangeToMono() 또는 exchangeToFlux() 를 사용하길 권한다.
(exchangeToMono()와 exchangeToFlux()는 사용이 완료된 Contents에 대해서는 자동으로 정리된다고 한다.)
b. Mono And Flux
WebClient 를 사용하면 Mono와 Flux 개념이 나오게 된다.
이는 Rective Stream의 Publisher 에 해당하는 것인데,
Spring.io에 연결된 설명 링크에는 Reactive Streams는 Non-Blocking Back Pressure를 사용하여 비동기식 스트림 처리에 대한 표준을 제공하기 위한 Initiative 라고 한다.
(Reactive Stream은 JAVA9 버전부터 나온 기능이다.)
이 부분에 대한 좀 더 자세한 설명은 Java Reactive Stream, Spring WebFlux 에 대한 학습 후 설명하도록 하겠다.
간단히 말하면 위에서 Mono 와 Flux 는 Publisher 즉 제공자 라고 볼수 있는데,
이를 Subscribe 을 하면 실행이 되는 구조이다.
그럼 둘의 차이는 무엇일까?
Mono 는 0~1 개의 element 를 반환
Flux 는 0~n 개의 element 를 반환
쉽게 생각하면 String 과 List<String> 의 차이를 생각하면 된다.
(예시는 예시일 뿐 실제와는 차이가 있으니 조금 더 공부해보도록 하자!)
예를 아래와 같이 Body에 JSON 구조의 단일 값을 받는다고 가정했을때,
{
"movieInfoResult":{
"movieInfo":{...},
"source":"영화진흥위원회"
}
}
코드에서 볼 수 있듯이, Mono 를 사용하게 된다.
public Mono<MovieInfoResponse> getMovieDetailFromApi(String movieCd) {
return webClient.get()
.uri(uriBuilder ->
uriBuilder
.path(PathType.MOVIE_DETAIL.getPath())
.queryParam("key", SERVICE_KEY)
.queryParam("movieCd", movieCd)
.build()
)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(MovieInfoResponse.class);
}
'그럼 Mono를 Flux 로 묶어서 사용이 가능하지 않을까?' 라는 생각이 들어 해당 내용을 찾아보고 테스트해 본 결과 가능했다.
코드에서 볼수듯이 movieCds 라는 String 값을
getMovieDetailFromApi() 라는 Mono를 반환하는 메소드에 파라메터로 넣어줌으로써,
단일 Mono 여러 개를 하나의 Flux 로 묶는 것이 가능했다.
List<String> movieCds = movies.stream()
.map(MovieResDto::getMovieCd)
.collect(Collectors.toList());
// Mono List -> Flux
Flux<MovieInfoResponse> movieDetailFlux = Flux.fromIterable(movieCds)
.flatMap(this::getMovieDetailFromApi);
4. 마무리
이번 포스팅에서는 대략적인 구조에 대해서만 정리하였다.
다음 포스팅에서는 WebClient에서 제공하는 기능에 대해서 다뤄볼 생각이다.
5. 참고문서
https://docs.spring.io/spring-framework/reference/web/webflux-webclient.html
https://www.baeldung.com/java-reactor-flux-vs-mono
https://ksr930.tistory.com/258
https://rieckpil.de/spring-webclient-exchange-vs-retrieve-a-comparison/
'DEV > Spring' 카테고리의 다른 글
[Spring] 권한 순서 정하기 (RoleHierarchy : 역할계층) (0) | 2023.09.17 |
---|---|
[Spring] API Path Prefix 설정하기 (0) | 2023.09.14 |
[Spring] Data Rest Event 사용하기 (0) | 2023.08.22 |
[JPA] @JoinColumns 두 개 이상의 조인 컬럼 설정하기 (0) | 2023.08.06 |
[Spring] 간편로그인 구현하기 (feat. 구글, 카카오) (0) | 2023.07.28 |