Spring Framework
Spring Webflux - WebClient
흥부가귀막혀
2022. 3. 14. 12:55
WebClient
생성
create
- WebClient 는 간단하게 static 메서드로 생성이 가능하다.
- static method 로 생성할 경우 디폴트로 Reactor Netty HttpClient 를 사용하므로 reactor-netty 라이브러리에 대한 의존성이 필요하다.
WebClient webClient1 = WebClient.create();
WebClient webClient2 = WebClient.create("http://naver.com");
builder
- 몇가지 옵션을 설정하려면 builder 를 사용하여 생성하는것을 권장한다.
- uriBuilderFactory: base URL 과 관련하여 UriBuilderFactory 를 커스텀하게 구현했을 경우.
- defaultUriVariables: 모든 요청에 핸들링 가능한 URI variable 설정.
- defaultHeader: 모든 요청에 사용할 header 설정.
- defaultCookie: 모든 요청에 사용할 cookie 설정.
- defaultRequest: 모든 요청의 request 를 커스터마이징 하는 설정.
- filter: 모든 요청의 filter 설정.
- exchangeStrategies: HTTP message reader/writer 의 커스터마이징 설정.
- clientConnector: HTTP client library 설정.
WebClient client = WebClient.builder()
.exchangeStrategies(builder -> {
return builder.codecs(codecConfigurer -> {
//...
});
})
.build();
- WebClient 는 한번 build 를 수행하면 상태를 변경할 수 없다. 기존 WebClient 에 설정을 변경하고 싶다면, mutate() 함수를 통해 복제를 하여 설정을 추가한다.
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
중요한 Configuration
Connection
- WebClient 에서 사용할 Connection 관련 설정은 ConnectionProvider.Builder 를 통해 설정하여 ConnectionProvider 를 생성한 뒤 Reactor Netty HttpClient 생성시 주입하면 된다.
- ConnectionProvider.Builder 를 통해 설정할 수 있는 Connection 관련 설정은 다음과 같다.
- evictInBackground : background thread 가 connection 종료 체크를 얼마나 자주 할건지에 대한 설정.
- maxConnections : max connection 수. WebClient 에서 connection 수 이상의 요청을 하게될 경우 queue 에 들어가 대기하게 된다.
- pendingAcquireMaxCount : connection 이 없을 때 queue 에 요청을 담을 최대 갯수. -1 로 설정할 경우 제한 없음.
- pendingAcquireTimeout : connection 이 없어 queue 에 담아놓을 시간(단위: ms).
- maxIdleTime : connection 의 최대 idle time(단위: ms). -1 일 경우 제한 없음.
- maxLifeTime : connection 의 최대 life time(단위: ms). -1 일 경우 제한 없음.
- getLeasingStrategy : Connection Acquire operation 수행시 connection 대여 전략. fifo, lifo 가 있다.(default: fifo)
- metrics : Connection 관련 metric 활성화 여부. 활성화를 할 경우 /actuator/metrics 를 통해 metric 확인 가능.
ConnectionProvider.Builder connectionProviderBuilder = ConnectionProvider.builder(name);
// 설정 수행
...
HttpClient httpClient = HttpClient.create(connectionProviderBuilder.build());
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
MaxInMemorySize
- 스프링 웹플럭스는 어플리케이션 메모리 이슈를 방지하기 위해 코덱의 메모리 버퍼 사이즈를 제한한다. 디폴트는 256KB로 설정돼 있는데, 버퍼가 부족하면 다음과 같은 에러가 발생한다.
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
- codec 설정을 통해 MaxInMemorySize 를 수정할 수 있다.
- -1 로 설정할 경우 무제한으로 설정된다.
WebClient webClient = WebClient.builder()
.exchangeStrategies(builder ->
builder.codecs(codecs ->
codecs.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)
)
)
.build();
Timeout
- Connect, Read/Write Timeout 은 Reactor Netty HttpClient 생성을 통해 다음과 같이 설정할 수 있다.
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.tcpConfiguration(client ->
client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(10000, TimeUnit.MILLISECONDS))
.addHandlerLast(new WriteTimeoutHandler(10000, TimeUnit.MILLISECONDS))));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Secure
- 타겟 서버의 ssl 인증서 체크를 무시하고자 한다면 아래와 같이 설정한다.
HttpClient httpClient = HttpClient.create()
.secure(
sslContextSpec -> {
try {
sslContextSpec.sslContext(
SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
);
} catch (SSLException e) {
throw new ServiceException("Insecure sslContext create fail.", e);
}
}
);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Metric
- WebClient 의 요청 url 별로 metric 을 얻고 싶다면 Reactor Netty HttpClient 생성시 metrics 함수를 통해 metric 활성화 및 url 별 tag 설정을 해준다.
- tag 설정이 되었다면 /actuator/metrics/{http.client.metric.key}?tag=key:value 형태로 요청하여 tag 에 맞는 metric 조회가 가능하다.
HttpClient httpClient = HttpClient.create()
.metrics(properties.isMetricsEnabled(), uri -> {
// uri 별 tag name 정의
return "my-api";
})
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
Request Body
- Mono, Flux 를 통해 비동기적으로 request body serialize & write 가 가능하다
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
- 비동기적으로 하지 않아도 된다면, bodyValue 함수를 사용한다.
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
- form data 의 경우 MultiValueMap<String, String> 형태로 bodyValue 를 호출하거나, BodyInserters 를 사용한다.
// bodyValue 사용
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
// BodyInserters 사용
Mono<Void> result = client.post()
.uri("/path", id)
.body(BodyInserters.fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
⚠️ Form Data 를 보낼 경우 FormHttpMessageWriter 에서 자동으로 Content-Type 을 application/x-www-form-urlencoded 으로 정의하기 때문에 contentType 함수를 사용해서 정의하지 않아도 된다.
Request & Response Handle
retrive()
- retrive() 는 response body 를 받아 디코딩하는 간단한 함수이다.
- 4xx, 5xx 응답 코드를 받으면 디폴트는 WebClientResponseException 또는 각 HTTP 상태에 해당하는 WebClientResponseException.BadRequest, WebClientResponseException.NotFound 등의 하위 exception을 던진다.
- http status 별 핸들링을 다르게 하고 싶다면 onStatus 메소드로 커스텀할 수도 있다.
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
⚠️ response body 가 있을 경우 onStatus 에서 소비하지 않으면 onStatus 호출 이후 response body 가 리소스 반환을 위해 비워지기 때문에 response body 를 볼 수 없게 된다. onStatus 를 사용하게 될 경우 response body 도 반드시 소비해야한다.
exchange()
- 직접 ClientResponse 를 핸들링 해야할 필요가 있다면 exchange() 함수를 통해 요청한다.
- exchange() 함수는 retrive() 함수와는 다르게 4XX, 5XX http status 에 대해 자동으로 에러 응답을 주지 않는다. 사용자가 직접 http status 를 확인하고 핸들링 하는 로직을 구현해야한다.
특정 model 객체로 변환시
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.bodyToMono(Person.class));
ResponseEntity 로 변환시
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response -> response.toEntity(Person.class));
⚠️ retrieve()와는 다르게 exchange()는 모든 시나리오에서(성공, 오류, 예기치 못한 데이터 등) 어플리케이션이 직접 response body를 컨슘해야 한다. 그렇지 않으면 메모리 릭이 발생할 수 있다. ClientResponse javadoc을 참고하면 body를 컨슘할 수 있는 모든 옵션이 나와 있다.