WebClient 사용할때 주의 (3편)
response body가 필요 없을 때
response body 가 필요없을 때 releaseBody(), toBodilessEntity(), bodyToMono(Void.class) 세가지 방법이 있다.
그런데 bodyToMono(Void.class) 는 한가지 문제가 있다. reactor-netty 는 bodyToMono(Void.class) 썼을 때 맺었던 커넥션을 커넥션풀에 반납을 하지 못한다. 왜냐하면 reactor-netty 는 통신하고 있는 서버에서 더 받을게 있는지 여부를 모르기 때문이다. 따라서 이 경우 reactor-netty는 해당 커넥션을 닫고 커넥션풀에서 제거한다.
그래서 reactor-netty 메인테이너는 releaseBody(), toBodilessEntity() 두가지만 추천한다. bodyToMono(Void.class) 와 다르게 spring framework 에서 drain 해주기 때문이다.
response body가 필요없어서 bodyToMono(Void.class) 를 사용했는데, 서버에서 더 받을게 있는지 여부를 모른다는 말이 이해안갈 수 있다. 이것은 bodyToMono(Void.class) 를 사용했음에도 만약 response body를 받았을 때에 해당되는 얘기다. response body를 받게 되면 커넥션은 닫히지만 커넥션 풀로 반납은 못한다.
참고: https://www.youtube.com/watch?v=LLSln1_JAMY&t=1668s
2021년 11월 23일 내용 추가
5.3.0-RC2 에 반영된 패치 에서 bodyToMono(Void.class) 커넥션풀 반납 이슈를 해결했다. 해당 패치는 2020.09.26에 이뤄졌고 reactor-netty 메인테이너 유튜브영상은 2020.09.25 업로드되어서 reactor-netty 메인테이너가 몰랐을거 같다는 생각이 들어서 다시 물어봤고 확인받았다. 즉 bodyToMono(Void.class) 를 사용해도 문제가 없다.
아래 DefaultWebClient(5.3.0-RC2) 를 보면 기존 drainBody 하는걸 바꿨다.
인코딩 중복 이슈
일반적으로 webClient 에서 baseUrl 로 scheme과 domain, port 정보를 입력하고, uriBuilder 를 이용해 path 나 query 를 추가한다.
WebClient.builder()
.clientConnector(connector)
.baseUrl("http://localhost:8080")
.build();
...
this.webClient
.get()
.uri(uriBuilder -> uriBuilder
.path("/api")
//.query("name=양봉수")
.query("name=%EC%96%91%EB%B4%89%EC%88%98")
.build()
)
.retrieve()
baseUrl 을 적고 build 를 하게 되면 내부적으로 UriComponentsBuilder 객체에 scheme, host 등의 정보로 들어가게 된다.
그래서 uriBuilder 를 통해서 만들게 되면 내부적으로 UriComponentsBuilder 객체에 이미 scheme 과 host 정보가 있어서 합쳐지게 된다.
uri 메서드 람다 바디안 uriBuilder 를 build 하면 내부적으로 DefaultUriBuilderFactory 에서 this.uriComponentsBuilder.build().expand(uriVars) 가 수행되고 디폴트로 인코딩이 안됐다고(false) 설정된다.
public UriComponents build() {
return build(false);
}
그래서 uriBuilder 를 통해 만들면 인코딩을 해서 보낸다는것을 꼭 주의해야 한다. 위의 예제에서 query name이 양봉수로 인코딩이 안되어 있으면 알아서 인코딩 해주므로 좋지만 만약 인코딩되어 있는 값을 넣는다면 또 인코딩이 되므로 query 변수값을 신경써야 한다.
그렇다면 인코딩을 안하게 하려면 어떻게 해야할까?
.uri(uriBuilder -> {
UriComponentsBuilder test = UriComponentsBuilder.newInstance();
test.fromUriString(FULL_URL)
return test.build(true).toUri();
});
위와 같이 UriComponentsBuilder 를 newInstance 로 새롭게 만들어서 build 할때 true 를 넘겨서 인코딩 됐다고 판단 하는 방법이 있다. 그런데 baseUrl 로 미리 등록해놓은걸 사용할 수 없고(기존 UriComponentsBuilder 를 사용 안하고 새로 만들었기 때문에)
람다 바디에서 UriBuilder 를 받아서 URI 를 리턴해야 되는데 UriBuilder 를 이용안하기 때문에 썩 맘에 들지 않는다.
그래서 두번째로 시도한 방법은 기존 UriBuilder 를 이용하는 방법이다.
.uri(uriBuilder -> {
DefaultUriBuilderFactory test (DefaultUriBuilderFactory.DefaultUriBuilder)uriBuilder;
test.uriString(PATH + "?" + QUERY_STRING);
test.setEncodingMode(EncodingMode.NONE);
return test.builder().build();
});
위와 같이 기존 UriBuilder 구현체인 DefaultUriBuilderFactory.DefaultUriBuilder 로 타입캐스팅해서 사용하면 baseUrl 을 통해 scheme, host 정보는 이미 존재하고 나머지 부분만 uriString 메서드로 채우면 된다. 그리고 setEncodingMode 메서드를 통해 인코딩 여부를 설정할 수 있다. 하지만 이방법은 사용이 불가능했다. 왜냐하면 DefaultUriBuilderFactory.DefaultUriBuilder 클래스가 private 이어서 가져다 사용할 수 없기 때문이다.
그래서 최종적으로는 URI.create() 로 새롭게 URI 객체를 만들어서 셋팅하게 작업했다(마찬가지로 baseUrl 로 미리 등록해놓은걸 사용할 수 없지만)
URI uri = URI.create(FULL_URL);
this.webClient
.get()
.uri(uri)
.retrieve()