[Spring] WebClient 사용방법 가이드
아래의 글 순서대로 읽으시면 해당 글을 이해하시는데 족흠 도움이 됩니다 🙏
⚙️ Block, Non-Block, sync(동기), Async(비동기) 의 간단한 개념
[Spring] Webclient 란❓ (RestTemplate vs WebClient)
[Spring] WebClient 사용방법 가이드 (now)
[spring] WebFlux란 + Reactor 객체란 (Mono<> 와 Flux<>)
저번 포스팅에 이어서 WebClient 사용방법에 대해 공부해보겠습니다.
📌 Spring WebClient 사용하기
공식문서에 나온내용을 토대로 해석해놓은 블로그글이 굉장히 많기 때문에
공식문서를 하나씩 보기보다는, 간단한 사용방법과 제가 어려웠던 부분을 토대로 정리를 해보겠습니다.
1) WebClient 종속성 추가하기
- Spring WebFlux를 추가하면 WebClient를 사용할 수 있습니다 (mvn 레파지토리)
<!-- https://mvnrepository.com/artifact/org.springframework/spring-webflux -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<version>5.3.23</version>
</dependency>
2) WebClient 객체 생성하기
WebClient에 구현된 정적 팩토리 메소드를 이용해, uri 를 생성시 설정해주거나, 설정없이 생성할 수 있습니다.
- WebClient.create()
- WebClient.create(String baseUrl)
- WebCleint.build() - create() 가 default 설정이라면, build() 를 이용해 커스텀하게 설정을 변경할 수 있습니다.
💡 WebClient 는 기본적으로 Immutable 하게 생성되므로 싱클톤으로 객체를 생성해서 설정을 그때그때 변경해서 사용할려면 mutable() 속성을 사용하여 생성할 수 있습니다.
✅ WebClient Build() 로 객체 얻어오기
public class WebClientUtil {
public static WebClient getBaseUrl(final String uri) {
return WebClient.builder()
.baseUrl(uri)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.build()
.mutate()
.build();
}
}
@Test
@DisplayName("WebClient build Singleton 사용하기 - kakao api get")
void getKakaoToken() {
WebClient webClient = WebClientUtil.getBaseUrl(REDIRECT_URI);
String token = webClient.get()
.uri(
"/oauth/authorize?client_id=" + KAKAO_REST_API_KEY
+ "&redirect_uri=" + REDIRECT_URI
+ "&response_type=code")
.retrieve()
.bodyToMono(String.class)
.block();
System.out.println("token = " + token);
Assertions.assertThat(token).isNotNull();
}
✅ Create()로 바로 사용하기
@Test
@DisplayName("WebClient create 사용")
void create(){
WebClient.create()
.get()
.uri("")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body()
.retrieve()
.bodyToMono(String.class)
.block();
}
👏🏻 여기서 block 은 비동기 논블락 방식으로 Mono 나 Flux 객체를 동기식 데이터 객체로 받아오기위한 메소드입니다.
3) WebClient Post Request / Response 핸들링하기
- get(), post(), put(), patch() 다 비슷하기 때문에 Webclient 로 post 사용방법만 알아 보겠습니다.
- WebClient를 사용하여 응답을 받는 방식으로 retrive() 와 exchange() 2가지 매소드를 이용할 수 있습니다.
🔥 retrive() vs exchage()
- retrvive( ) : retrive() 메소드는 CleintResponse 개체의 body를 받아 디코딩하고 사용자가 사용할 수 있도록 미리 만든 개체를 제공하는 간단한 메소드 입니다.
- exchage( ) : ClientResponse를 상태값, 헤더와 함께 가져오는 메소드입니다.
➡️ retrive() 는 예외를 처리하기 좋은 api 를 가지고 있지않아, 응답 상태 및 헤더와 함게 더 세밀한 조정이 하고싶을 떄는 exchange를 사용하라고 합니다.
➡️ 하지만, exchange()를 통해 세밀한 조정이 가능하지만, Response 컨텐츠에 대해 모든 처리를 직접 하게되면 메모리 누수가 발생할 가능성이 존재하기 때문에 retrive()를 권장한다고 합니다.
📌 retrive( )
- retrive() 를 사용할 때는, toEntity(), bodyToMono(), bodyToFlux() 이렇게 response를 받아올 수 있습니다.
- bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔줍니다.
1. 먼저 가장 기본적인 방법으로 retrive() 와 toEntity() 를 이용해 ResponseEntity<T> 로 받는 방법입니다.
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
2. 당연히 헤더를 제외한, Body 값만 받을 수 도 있습니다.
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
3. Block() 을 이용해서 Mono 나 Flux가 아닌 동기식 스타일 일반 객체로 받을 수도 있습니다.
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
📌 exchange( )
- deprecated 되었습니다.
- 메모리 누수 및/또는 연결 가능성으로 인해 5.3부터 'exchangeToMono(Function)' , 'exchangeToFlux(Function)' 를 사용하라고 합니다.
- 또한 오류 상태 처리와 함께 ResponseEntity 를 통해 응답 상태 및 헤더에 대한 액세스를 제공하는 retrieve() 사용을 권장합니다!
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
return response.createException().flatMap(Mono::error);
}
});
Flux<Person> entityMono = client.get()
.uri("/persons")
.accept(MediaType.APPLICATION_JSON)
.exchangeToFlux(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToFlux(Person.class);
}
else {
return response.createException().flatMapMany(Mono::error);
}
});
📌 Mono, Flux 사용 방법
- 위에서 사용했던 block 방식과 non blocking 방식으로 사용할 수 있습니다.
- Non-Blocking 방식으로 처리하고자 할 때에는 .subscribe() 를 통해 callback 함수를 지정할 수 있습니다.
// blocking
Mono<Employee> employeeMono = webClient.get(). ...
employeeMono.block()
// non-blocking
Mono<Employee> employeeFlux = webClient.get(). ...
employeeFlux.subscribe(employee -> { ... });
📌 알리고 API 사용 예제, post()
👏🏻 저는 알리고 토큰 생성 API를 예제로 진행해보았습니다.
- 알리고에서는 Post 로 formData 에 값을 넣어서 요청하면, Json 형태로 응답값을 준다고 API 스펙에 명시되어있습니다.
[Request 양식]
curl -X POST "https://kakaoapi.aligo.in/akv10/token/create/30/s/" \
--data-urlencode "apikey=xxxxx" \
--data-urlencode "userid=xxxxx"
[Response 양식]
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"code": 0
"message": "정상적으로 생성하였습니다."
"token": tokeeeeeeeeeeennnnnnncoooooodoe
"urlencode": tokeeeeeeeeeeennnnnnncoooooodourlllllllelela
}
Json 으로 들어오는 Body만 받기위해서 Dto 객체를 새롭게 만들어 주었습니다.
@Getter
@NoArgsConstructor
public class AligoApi {
Integer code;
String message;
String token;
String urlencode;
}
Json 바디값을 객체로 받아오기위해서, 생성자나 Setter 가 필요하진 않았습니다. 이유는 저도 모르겠군여..
🔥그 이유는, bodyToMono(Class<T> bodyResponse) 에서 생성자를 "리플렉션" 으로 생성해주었기 때문입니다.
이제 Webclient post 예제입니다.
- 제가 사용한 방법이니 더 좋은 방법있으면 알려주세요~!
public String generateToken() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("apikey",APIKEY);
formData.add("userid",USER_ID);
Mono<AligoApi> result = WebClient.create().post()
.uri(ALIGO_HOST + GENERATE_TOKEN_URL)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(BodyInserters.fromFormData(formData))
//혹은 바로 formData를 생성할 수 있습니다.
//.body(BodyInserters.fromFormData("apikey",APIKEY).with("userid",USER_ID))
.retrieve()
.bodyToMono(AligoApi.class)
.timeout(Duration.ofMillis(1000))
.blockOptional().orElseThrow(
() -> new AliGoAPICallException("알리고 토큰을 생성하지 못했습니다.")
);
return response.getToken();
}
알리고 응답 실페 케이스
- 생성자 없어도, null 값이 잘 들어옵니다.. 정말 신기하네
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"code": -99
"message": "계정 아이디(=userid) 파라메더 정보가 전달되지 않았습니다."
}
🙏 retrive 이냐 exchagne 이냐에 따라서 errorHandling 방법이 달라집니다. 저는 이것에 대해 다루지는 않지만
아래 참조 중 gngsn 님의 WebClient 가이드링크에 상세하게 나와있습니다.
WebClient 는 Spring Reactor를 기반으로 하고있기 때문에 block, Non-Block & 동기, 비동기의 개념
또 그것을 다루는 Mono 와 Flux 객체의 개념을 알아야한다는 생각이 드는군요.
다음에는 이것에 대해 공부해보자!!
💡 참고
- WebClient Spring 공식문서 : https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
- mono flux 개념 : https://github.com/gngsn/Gngsn-Spring-Lab/blob/master/note/Reactor.md
- WebClient 가이드 : https://gngsn.tistory.com/154
- 예제1 : https://howtodoinjava.com/spring-webflux/webclient-get-post-example/
- 예제 2 stackoverFlow : https://stackoverflow.com/questions/59792224/how-to-post-request-with-spring-boot-web-client-for-form-data-for-content-type-a