일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- 개발자 면접
- 직장인점심구독
- 셰프의찾아가는구내식당
- Linux
- 개발자 인턴
- 자바 암호화 알고리즘
- 플레이팅
- 개발자 채용연계형인턴
- 개발자 정규직
- 샐러드
- .dockerignore
- 도커
- 신입 개발자 필수 면접 질문
- 식단
- 신입 개발자 면접
- docker
- docker image
- 코딩테스트 연습
- 신입 개발자 자바 면접 질문
- Plating
- 자바 암호화 복호화
- 프로그래머스 코딩테스트
- 개발자 코딩테스트
- 신입 개발자 자바 면접
- 자바 암호화
- 식단일기
- 개발자 면접 질문
- 신입 개발자 면접 질문
- 프로그래머스
- 자바 stream
- Today
- Total
달리는 두딘
[Spring] WebClient 사용법 본문
Spring 어플리케이션에서 HTTP 요청을 할 땐 주로 RestTemplate 을 사용했었습니다.
하지만 Spring 5.0 버전부터는 RestTemplate 은 유지 모드로 변경되고 향후 deprecated 될 예정입니다.
RestTemplate 의 대안으로 Spring 에서는 WebClient 사용을 강력히 권고하고 있으며 다음과 같은 특징을 가지고 있습니다.
- Non-blocking I/O
- Reactive Streams back pressure
- High concurrency with fewer hardware resources
- Functional-style, fluent API that takes advantage of Java 8 lambdas
- Synchronous and asynchronous interactions
- Streaming up to or streaming down from a server
Reactive 환경과 MSA를 생각하고 있다면 WebClient 사용을 적극 권장해 드리며, 기본 설정부터 Method 별 사용법까지 차근차근 알아보도록 하겠습니다.
Configuration
WebClient 를 사용하기 위한 가장 간단한 방법은 static factory 를 통해 WebClient 를 생성해서 사용할 수 있습니다.
WebClient.create();
WebClient.create(String baseUrl);
하지만 default 값이나 filter 또는 ConnectionTimeOut 같은 값을 지정하여 생성하기 위해서는 Builder 클래스를 통해 생성하는 것이 좋습니다.
Builer() 를 통하면
- 모든 호출에 대한 기본 Header / Cookie 값 설정
- filter 를 통한 Request/Response 처리
- Http 메시지 Reader/Writer 조작
- Http Client Library 설정
등이 가능합니다.
Spring 여러 Bean 에서 사용하기 위해 @Configuration 을 통해 WebClient 를 선언합니다.
MaxInMemorySize
Spring WebFlux 에서는 어플리케이션 메모리 문제를 피하기 위해 codec 처리를 위한 in-memory buffer 값이 256KB로 기본설정 되어 있습니다. 이 제약 때문에 256KB보다 큰 HTTP 메시지를 처리하려고 하면 DataBufferLimitException 에러가 발생하게 됩니다. 이 값을 늘려주기 위해서는 ExchageStrategies.builder() 를 통해 값을 늘려줘야 합니다.
ExchangeStrategies exchangeStrategies =
ExchangeStrategies
.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize(1024*1024*50))
.build();
Logging
Debug 레벨 일 때 form Data 와 Trace 레벨 일 때 header 정보는 민감한 정보를 포함하고 있기 때문에, 기본 WebClient 설정에서는 위 정보를 로그에서 확인할 수 가 없습니다. 개발 진행 시 Request/Response 정보를 상세히 확인하기 위해서는 ExchageStrateges 와 logging level 설정을 통해 로그 확인이 가능하도록 해 주는 것이 좋습니다.
exchangeStrategies
.messageWriters().stream()
.filter(LoggingCodecSupport.class::isInstance)
.forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));
ExchangeStrategies 를 통해 setEnableLoggingRequestDetails(boolen enable) 을 true 로 설정해 주고 application.yaml 에 개발용 로깅 레벨은 DEBUG 로 설정해 줍니다.
logging:
level:
org.springframework.web.reactive.function.client.ExchangeFunctions: DEBUG
Client Filters
Request 또는 Response 데이터에 대해 조작을 하거나 추가 작업을 하기 위해서는 WebClient.builder().filter() 메소드를 이용해야 합니다. ExchangeFilterFunction.ofRequestProcessor() 와 ExchangeFilterFunction.ofResponseProcessor() 를 통해 clientRequest 와 clientResponse 를 변경하거나 출력할 수 있습니다.
Request / Response header를 출력하는 예제를 다음과 같이 설정 할 수 있습니다.
WebClient.builder()
.filter(ExchangeFilterFunction.ofRequestProcessor(
clientRequest -> {
log.debug("Request: {} {}", clientRequest.method(), clientRequest.url());
clientRequest.headers()
.forEach((name, values) -> values.forEach(value -> log.debug("{} : {}", name, value)));
return Mono.just(clientRequest);
}
))
.filter(ExchangeFilterFunction.ofResponseProcessor(
clientResponse -> {
clientResponse.headers()
.asHttpHeaders()
.forEach((name, values) ->
values.forEach(value -> log.debug("{} : {}", name, value)));
return Mono.just(clientResponse);
}
))
HttpClient TimeOut
HttpClient 를 변경하거나 ConnectionTimeOut 과 같은 설정값을 변경하려면 WebClient.builder().clientConnector() 를 통해 Reactor Netty의 HttpClient 를 직접 설정해 줘야 합니다.
WebClient
.builder()
.clientConnector(
new ReactorClientHttpConnector(
HttpClient
.create()
.secure(
ThrowingConsumer.unchecked(
sslContextSpec -> sslContextSpec.sslContext(
SslContextBuilder
.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build()
)
)
)
.tcpConfiguration(
client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 120_000)
.doOnConnected(
conn -> conn.addHandlerLast(new ReadTimeoutHandler(180))
.addHandlerLast(new WriteTimeoutHandler(180))
)
)
)
)
new ReactorClientHttpConnector() 를 통해 옵션이 추가된 새로운 HttpClient 를 설정해 줍니다.
위 예제에서는 HTTPS 인증서를 검증하지 않고 바로 접속하는 설정과, TCP 연결 시 ConnectionTimeOut , ReadTimeOut , WriteTimeOut 을 적용하는 설정을 추가하였습니다.
Stream 처리에서 Exception 처리를 위한 Util인 ThrowingConsumer 는 여기를 참고하시길 바랍니다.
Usage
WebClient 는 기존 설정값을 상속해서 사용할 수 있는 mutate() 함수를 제공하고 있습니다. mutate() 를 통해 builder() 를 다시 생성하여 추가적인 옵션을 설정하여 재사용이 가능하기 때문에 @Bean 으로 등록한 WebClient는 각 Component 에서 의존주입하여 mutate()를 통해 사용 하는 것이 좋습니다.
WebClient a = WebClient.builder()
.baseUrl("https://some.com")
.build();
WebClient b = a.mutate()
.defaultHeader("user-agent", "WebClient")
.build();
WebClient c = b.mutate()
.defaultHeader(HttpHeaders.AUTHORIZATION, token)
.build();
위와 같이 설정 했을 경우 WebClient “c”는 “a”와 “b”에 설정된 baseUrl , user-agent 해더를 모두 가지고 있습니다.
@Bean 으로 등록된 WebClient 는 다음과 같이 사용 가능합니다.
@Service
@RequiredArgsConstructor
@Slf4j
public class SomeService implements SomeInterface {
private final WebClient webClient; public Mono<SomeData> getSomething() {
return webClient.mutate()
.build()
.get()
.uri("/resource")
.retrieve()
.bodyToMono(SomeData.class);
}
}
retrieve() vs exchange()
HTTP 호출 결과를 가져오는 두 가지 방법으로 retrieve() 와 exchange() 가 존재합니다. retrieve 를 이용하면 바로 ResponseBody를 처리 할 수 있고, exchange 를 이용하면 세세한 컨트롤이 가능합니다. 하지만 Spring에서는 exchange 를 이용하게 되면 Response 컨텐츠에 대한 모든 처리를 직접 하면서 발생할 수 있는 memory leak 가능성 때문에 가급적 retrieve 를 사용하기를 권고하고 있습니다.
- retrieve
Mono<Person> result = webClient.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
- exchange
Mono<Person> result = webClient.get()
.uri("/persons/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMap(response ->
response.bodyToMono(Person.class));
4xx and 5xx 처리
HTTP 응답 코드가 4xx 또는 5xx로 내려올 경우 WebClient 에서는 WebClientResponseException이 발생하게 됩니다. 이 때 각 상태코드에 따라 임의의 처리를 하거나 Exception 을 랩핑하고 싶을 때는 onStatus() 함수를 사용하여 해결 할 수 있습니다.
webClient.mutate()
.baseUrl("https://some.com")
.build()
.get()
.uri("/resource")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(status -> status.is4xxClientError()
|| status.is5xxServerError()
, clientResponse ->
clientResponse.bodyToMono(String.class)
.map(body -> new RuntimeException(body)))
.bodyToMono(SomeData.class)
GET
그럼 이제부터 HTTP GET 을 호출 하는 법을 알아보겠습니다.
GET 호출은 앞서 보여진 예시에서 처럼 get() 함수를 통해 사용되며 uri() 를 통해 호출 리소스 정보를 전달해 줘야 합니다. 만약 Query 파라미터가 존재한다면 다음과 같이 변수를 추가 줄 수 있습니다.
public Mono<SomeData> getData(Integer id, String accessToken) {
return
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.get()
.uri("/resource?id={ID}", id)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.retrieve()
.bodyToMono(SomeData.class)
;
}
uri() 함수를 제공하는 UriSpec 인터페이스는 아래와 같으며, Map 을 이용하거나 직접 UriBuilder 등을 통해 세세한 컨트롤도 가능합니다.
POST
Body Contents를 전송할 수 있는 POST 호출 은 post() 함수를 통해 제공되며, RequestBody를 설정하기 위한 RequestBodySpec 인터페이스는 다음과 같습니다.
가장 많이 사용되는 form 데이터 전송과 JSON Body 데이터 전송을 간단한 예로 알아보겠습니다.
- form 데이터 전송
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.post()
.uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.accept(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData("id", idValue)
.with("pwd", pwdValue)
)
.retrieve()
.bodyToMono(SomeData.class);
form 데이터를 생성하기 위해서는 BodyInserters.fromFormData() 를 이용할 수 있으며, bodyValue(MultiValueMap<String, String>) 을 통해서도 데이터를 전송 할 수 있습니다.
- JSON body 데이터 전송
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.post()
.uri("/login")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(loginInfo)
.retrieve()
.bodyToMono(SomeData.class);
객체 자체를 RequestBody로 전달하기 위해서는 bodyValue(Object body) 를 사용하거나 body(Object producer, Class<?> elementClass) 를 통해서 사용할 수 있습니다.
또한 Mono 나 Flux 객체를 통해 RequestBody를 생성하기 위한
<T, P extends Publisher<T>> RequestHeadersSpec<?> body(P publisher, Class<T> elementClass);
함수도 존재합니다.
PUT
PUT 호출은 POST 호출과 유사하며 다만 put() 함수를 통해 시작되는 것 만 다릅니다.
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.put()
.uri("/resource/{ID}", id)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.bodyValue(someData)
.retrieve()
.bodyToMono(SomeData.class);
DELETE
DELETE 호출은 GET 과 유사하며 delete() 함수를 통해 시작되고, delete() 함수 의 특성상 response는 Void.class 로 처리됩니다.
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.delete()
.uri("/resource/{ID}", id)
.retrieve()
.bodyToMono(Void.class);
Synchronous Use
WebClient 는 Reactive Stream 기반이므로 리턴값을 Mono 또는 Flux 로 전달받게 됩니다. Spring WebFlux를 이미 사용하고 있다면 문제가 없지만 Spring MVC를 사용하는 상황에서 WebClient 를 활용하고자 한다면 Mono 나 Flux 를 객체로 변환하거나 Java Stream 으로 변환해야 할 필요가 있습니다.
이럴 경우를 대비해서 Mono.block() 이나 Flux.blockFirst() 와 같은 blocking 함수가 존재하지만 block() 을 이용해서 객체로 변환하면 Reactive Pipeline 을 사용하는 장점이 없어지고 모든 호출이 main 쓰레드에서 호출되기 때문에 Spring 측에서는 block() 은 테스트 용도 외에는 가급적 사용하지 말라고 권고하고 있습니다.
대신 완벽한 Reactive 호출은 아니지만 Lazy Subscribe 를 통한 Stream 또는 Iterable 로 변환 시킬 수 있는 Flux.toStream() , Flux.toIterable() 함수를 제공하고 있습니다.
List<SomeData> results =
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.get()
.uri("/resource")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlux(SomeData.class)
.toStream()
.collect(Collectors.toList());
Flux.toStream() 을 통해 데이터를 추가 처리하거나 List로 변환하여 사용할 수 있습니다.
Mono 에 대해서는
SomeData data =
webClient.mutate()
.baseUrl("https://some.com/api")
.build()
.get()
.uri("/resource/{ID}", id)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(SomeData.class)
.flux()
.toStream()
.findFirst()
.orElse(defaultValue);
Mono.flux() 를 통해 Flux 로 변환하고 findFirst() 를 통해 Optional 처리 하는것이 좋습니다. (이 때는 onError 처리가 필요합니다)
WebClient 는 RestTemplate 에 비해 사용성도 뛰어나고, 대규모 호출에 대해서도 자원 및 성능상 이점이 많아 WebClient 를 점진적으로 도입해 보실 것을 권해드립니다.
출처
https://medium.com/@odysseymoon/spring-webclient-%EC%82%AC%EC%9A%A9%EB%B2%95-5f92d295edc0
'지식노트' 카테고리의 다른 글
[JAVA] 크론 표현식 (Cron Expression) 정리 (2) | 2022.09.15 |
---|---|
[JAVA] Stream - findAny()와 findFirst()의 차이점 (2) | 2022.09.06 |
[JAVA] Stream().filter, map, flatMap 사용 방법 및 예제 (2) | 2022.08.25 |
[DB] Lock이란? (0) | 2022.08.18 |
[기타] CI, DI란? (0) | 2022.08.11 |