해당 글은 spring-webmvc
에서 외부 호출만을 위해 spring-webflux
의 WebClient
를 사용하기 위한 설정에 대한 내용입니다.
MSA에서는 서비스간의 호출을 피할수 없는 숙명이다.
다음은 대표적인 MSA로 서비스중인 Amazon, Netfilx의 서비스간 API 호출 관계를 나타낸 그림이다. 대표적인 선생님 두분 모두 엄청나게 복잡한 서비스간의 호출관계를 지니고 있다. 이는 피할수 없는 문제인것 같은데 왜 현재 프로젝트에서는 타 서비스의 호출이 전혀 없으며 절대적인 호출관계의 레이어를 나누려 하는지 모르겠다. 나의 배움이 부족한것 같다.
위와 같이 다른 서비스의 호출은 필수적이지만 항상 먼저 키보드에 손이 가는 단어는 RestTemplate
이다. 하지만 현재 RestTemplate
는 유지 및 변경만 되고 있으며 향후 deprecated
된다는 설명이 있다.항상 먼저 RestTemplate에 손이 가는 벌써 옛날 개발자인가...
NOTE: As of 5.0 this class is in maintenance mode, with
- only minor requests for changes and bugs to be accepted going forward. Please,
- consider using the {@code org.springframework.web.reactive.client.WebClient}
- which has a more modern API and supports sync, async, and streaming scenarios.
- GitHub
우선 Bean
으로 동록하여 사용할 WebClient
를 생성하며 기본 설정들을 알아보려고 한다. 기본 설정들에 대한 간단한 내용은 스프링 문서에 친절하게 작성되어 있으며 이를 활용하여 이번 프로젝트에서 사용한 설정과 유용할 것으로 생각되는 설정을 알아보려고 한다.
maxInMemorySize(문서)
WebClient 사용시 항상 문제가 되었던 부분으로 WebClient
의 기본 input stream
의 버퍼 크기는 256k
라고 한다. 이러한 이유 때문에 통신의 데이터가 256K
보다 큰 경우 DataBufferLimitException
이 발생하였다. 상세한 내용은 maxInMemorySize doc를 확인하면 된다.
WebClient
.builder()
.baseUrl("http://localhost:8080")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
filter(문서)
filter는 Request(요청)
, Response(응답)
에 대해 intercept
를 통해 수정을 할 수 있다.
이를 통해서 수신한 Request(요청)
의 Header
의 값이 Forward
되어야 하는경우 filter
에서 복제하여 Request
를 다른 서버로 전달 할 수 있으며 Request
, Response
에 대한 로그도 작성할 수 있다.
ExchangeFilterFunction.ofRequestProcessor
, ExchangeFilterFunction.ofResponseProcessor
의 구현을 통해서 요청전과 응답후에 대해 수정이 가능하다.
WebClient
.builder()
.baseUrl("http://localhost:8080")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
log.info("####"+Thread.currentThread().getName());
log.info(clientRequest.url().toString());
ClientRequest.Builder from = ClientRequest.from(clientRequest);
for (Enumeration<?> e = servletRequestAttributes.getRequest().getHeaderNames(); e.hasMoreElements(); ) {
String nextHeaderName = (String) e.nextElement();
String headerValue = servletRequestAttributes.getRequest().getHeader(nextHeaderName);
if (nextHeaderName.equals("caller")) {
from.header(nextHeaderName, headerValue);
}
log.info(nextHeaderName + headerValue);
}
return Mono.just(from.build());
}).andThen(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
clientResponse.headers().asHttpHeaders().forEach((name, value) -> {
try {
log.info(name + new ObjectMapper().writeValueAsString(value));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
});
return Mono.just(clientResponse);
})))
.build();
여기서 약간의 의문점이 있는 점은 WebClient
는 Event Loop
를 통해서 Single Thread
에서 논블록킹
를 구현된다고 한다.
지금 프로젝트는 spring-webmvc
로 Tomcat
를 사용하고 있으며 단순히 타 서버 호출을 위해 WebFlux
의존성을 추가하여 기본 Netty
의 의존성도 추가되어 있다. 그러한 결과 서버 Thread
가 http-nio-8080-exec
과 reactor-http-nio
가 모두 존재하는것을 확인하였다.
의문점은 현재 RequestContextHolder
의 Header
값을 복제를 하여 다음 요청에도 추가해야 하는데 도대체 Thread
가 어느 시점에 전환되어 동작 되는지 너무 복잡하다. 우선ExchangeFilterFunction.ofRequestProcessor
부분에서 Thread
이름을 출력해본 결과 http-nio-8080-exec
으로 확인하였으며 ExchangeFilterFunction.ofResponseProcessor
부분에서는 reactor-http-nio
으로 확인하여 Multi Thread
를 이용하여 WebClient
를 비동기 호출하는 경우 많은 공부가 필요해 보인다. baeldung - pring-webflux-concurrency
위의 두개의 설정이외에 timeout등 많은 설정들에 대한 내용은 문서에 많은 예제들과 친절한 설명으로 작성되어있으며 참고하면 될것같다.
다음 글에서는 이렇게 생성한 WebClient
Bean
을 이용하여 다양한 Http Method
와 Media Type
에 따른 요청방법에 대해 작성할 예정이다.
@Configuration
@Slf4j
public class SampleWebClientConfig {
@Bean(name = "sampleWebClient")
public WebClient sampleWebClient() {
return WebClient
.builder()
.baseUrl("http://localhost:8080")
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
log.info("####"+Thread.currentThread().getName());
log.info(clientRequest.url().toString());
ClientRequest.Builder from = ClientRequest.from(clientRequest);
for (Enumeration<?> e = servletRequestAttributes.getRequest().getHeaderNames(); e.hasMoreElements(); ) {
String nextHeaderName = (String) e.nextElement();
String headerValue = servletRequestAttributes.getRequest().getHeader(nextHeaderName);
if (nextHeaderName.equals("caller")) {
from.header(nextHeaderName, headerValue);
}
log.info(nextHeaderName + headerValue);
}
return Mono.just(from.build());
}).andThen(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
clientResponse.headers().asHttpHeaders().forEach((name, value) -> {
try {
log.info(name + new ObjectMapper().writeValueAsString(value));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
});
return Mono.just(clientResponse);
})))
.build();
}
}