웹으로 API를 호출하기 위해 사용되는 Http Client 모듈 중 하나.
HTTP 클라이언트
라고 하는 것은 HTTP 프로토콜
을 이용하여 서버와 통신하는 것을 의미하며 다른 말로는 서버에 API 요청을 보내는 주체라고도 말할 수 있다.WebClient는 non-blocking(비동기) 방식으로 응답을 기다리지 않고, 응답이 왔을 때 처리를 해주는 방식이다.
따라서 사용자가 많아지면 많아질수록 RestTemplate의 성능은 점차 떨어진다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
이제 WebClient를 사용할 수 있게 됐으니 인스턴스를 생성하자.
🎈 인스턴스를 생성하는 방법에는 여러가지가 있는데 가장 기본적으로 인스턴스를 생성하는 방법은 아래와 같다.
WebClient client = WebClient.create();
여기서 인스턴스를 생성할 때 해당 WebClient의 BaseUrl(기본 주소)를 설정하고 싶으면 아래와 같이 해주면 된다.
WebClient client = WebClient.create('주소');
😎 여기서 baseUrl
은 외부 API의 기본 URL을 의미한다. 일반적으로 외부 API
는 특정 도메인 주소
와 API 엔드포인트
를 가지고 있다. baseUrl에 해당 도메인 주소를 넣어주면 돼 !
예를 들어, 가상의 외부 API인 "https://api.example.com"이 있다고 가정해봅시다. 이 API는 다양한 엔드포인트들을 가지고 있을 수 있습니다. 이 API에 접근하기 위해서는 기본 URL인 "https://api.example.com"을 baseUrl로 설정해야 합니다.
WebClient를 사용하여 외부 API를 호출할 때, baseUrl을 설정하면 각 API 호출에 사용되는 상대적인 경로를 지정하는 데 유용합니다. 즉, uri() 메서드로 상대 경로만 지정해도 WebClient는 자동으로 baseUrl과 합쳐서 완전한 API 엔드포인트 URL을 생성하여 API에 요청을 보냅니다.
⚽ 다시 말해서, baseUrl을 설정하면 매번 uri() 메서드를 호출할 때마다 완전한 URL을 직접 입력할 필요 없이 상대 경로만 입력하여 간편하게 API를 호출할 수 있습니다.
예를 들어, baseUrl
이 "https://api.example.com"으로 설정되어 있다면:
webClient.get().
uri("/data").
retrieve().bodyToMono(String.class)
.subscribe(...);
uri()
메서드에서 /data
만 입력해도 WebClient는 "https://api.example.com/data"로 완전한 URL을 만들어서 API를 호출합니다.baseUrl
은 WebClient를 사용하여 외부 API를 호출할 때, API의 기본 도메인 주소를 지정하는 중요한 설정 값입니다. 실제로 사용할 API의 기본 URL에 따라 baseUrl
을 설정해주어야 정상적으로 API 호출이 이루어집니다.😀 그리고 해당 WebClient의 기본 설정들을 해주고 싶다면 아래와 같이 해준다.
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
.build();
위 내용들은 BaseUrl, Cookie, Header, UriVariable을 설정한 것.
👻 API key를 보내야 한다면 (나같은 경우) 아래와 같이 만들어! (인코딩을 해야했기 때문)
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
WebClient client = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
이제 request 생성과 response 생성 하는 부분에 대해서 알아보자
인스턴스를 생성했으니, request와 response를 만들면 된다.
⚽ WebClient로 할 수 있는 요청은 GET, POST, PUT, DELETE이다.
String response = client.get()
.uri(uriBuilder -> uriBuilder.path("getVilageFcst") //uri 생성해주는 부분
.queryParam("serviceKey", serviceKey)
.queryParam("numOfRows", numOfRows)
.queryParam("pageNo", pageNo)
.queryParam("dateType", dateType)
.queryParam("base_date", base_date)
.queryParam("base_time", base_time)
.queryParam("nx", nx)
.queryParam("ny", ny)
.build()
).retrieve()//응답을 받게 하는 부분
.bodyToMono(String.class) //응답 받을 때 값을 하나만 받을 것이다.
.block(); //동기식으로 응답을 받을 것이다.
(위에서 API를 사용하기 위한 인증 key를 사용하기 위해 인코딩을 해야했기 때문에 DefaultUriBuilderFactory를 통해 인스턴스 얻어낸 이후이다.) → 물론 baseUrl 역시 등록 완료된 상태.
위 소스코드는 GET요청을 할 것이기 때문에 client.get()으로 했고, 그 밑에 .uri는 이제 BaseURI 뒤에 만들어질 URI를 만든 것이다.
일단은 UriBuilder로 uri를 Build하였고, path로 해당 API 메서드의 이름을 넣어주었다.
그리고 밑에 queryParam으로는 해당 API를 호출할 조건들을 같이 넣어줬다.
그리고 쿼리를 다 넣어줬다면 build()를 해서 uri를 만들어줬다.
그런 다음에 retrieve()를 통해서 응답을 받게 했다.
그리고 여기서 이제 bodyToFlux()냐 bodyToMono()이냐 이 둘 중에 하나를 택해야 하는데, 만약 응답 받을 값이 여러개이면 bodyToFlux()를 사용하고, 응답 받을 값이 한개면 bodyToMono()를 선택해주면 된다 !
위에서 응답 받을 값이 한개이기 때문에 bodyToMono()를 했고, 응답으로 받을 Class타입은 String이기 때문에 매개변수로 String.class를 입력했다.
그리고 나는 이번 호출 땐 한번의 호출만을 사용할 것이기 때문에 .block()을 사용하였다 !!
대충 전체적인 코드는 위와 같다.
그래도 공부가 더 필요하다..!!!
webClient를 이용하여 코드를 작성하다 보면 하나의 메서드 안에 여러 API 요청이 수행될 수 있다 .
또한 API에 데이터를 담다 보면 API마다 공통적으로 들어가야 하는 header값이나 cookie 등의 값들이 존재할 수 있다.
😎 코드를 보면서 확인해보자.
package com.webclient.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WebClientServiceImpl {
public void defaultValue() {
String code = "myCode";
// webClient 기본 설정
WebClient webClient =
WebClient
.builder()
.baseUrl("http://localhost:8080")
.build();
// api 요청 - 1
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.cookie("cookie", "cookieValue")
.retrieve()
.bodyToMono(Map.class)
.block();
// api 요청 - 2
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.cookie("cookie", "cookieValue")
.retrieve()
.bodyToMono(Map.class)
.block();
}
}
위 코드를 보면 API마다 공통적으로 header와 cookie가 들어가기 때문에 요청할 때마다 데이터를 담아서 전달해주고 있다.
하지만 매번 요청할 때마다 데이터를 담아주는 것은 불편한 반복 작업 중 하나이고, 이를 위한 개선 방법으로 webClient를 처음 생성할 때 default 값을 담아줄 수 있다 !
@Service
@Slf4j
public class WebClientServiceImpl {
public void defaultValue() {
String code = "myCode";
// webClient 기본 설정
WebClient webClient =
WebClient
.builder()
.baseUrl("http://localhost:8080")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultCookie("cookie", "cookieValue")
.build();
// api 요청 - 1
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.retrieve()
.bodyToMono(Map.class)
.block();
// api 요청 - 2
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.retrieve()
.bodyToMono(Map.class)
.block();
}
}
지금까지의 코드들을 보면 get, post 요청을 한 뒤에 .retrieve()라는 코드가 들어가 있는 것을 볼 수 있다.
여기서 사용되는 retrieve는 일반적으로 webClient로 request를 서버에 전달한 응답 값을 추출하기 위해 사용된다.
retrieve()
의 return 값으로는 총 3개가 있다.
👻 현재까지는 모두 코드를 bodyToMono들이 사용되었다.
나머지도 한번 사용해보자 !
@Service
@Slf4j
public class WebClientServiceImpl {
public void getEntity() {
String code = "myCode";
// webClient 기본 설정
WebClient webClient =
WebClient
.builder()
.baseUrl("http://localhost:8080")
.build();
// api 요청
ResponseEntity<Map> response =
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.retrieve()
.toEntity(Map.class)
.block();
// 결과 확인
log.info(response.toString());
}
}
@SpringBootTest
class WebClientApplicationTests {
@Autowired
private WebClientServiceImpl webClientService;
@Test
void getEntity() {
webClientService.getEntity();
}
}
INFO 13808 --- [ Test worker] c.w.service.WebClientServiceImpl : <200,{code=myCode, message=Success},[Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Wed, 15 Mar 2023 13:48:24 GMT"]>
@Service
@Slf4j
public class WebClientServiceImpl {
public void getFlux() {
String code = "myCode";
// webClient 기본 설정
WebClient webClient =
WebClient
.builder()
.baseUrl("http://localhost:8080")
.build();
// api 요청
List<String> response =
webClient
.get()
.uri(uriBuilder ->
uriBuilder
.path("/api/get")
.queryParam("code", code)
.build())
.retrieve()
.bodyToFlux(Map.class)
.toStream()
.map(map -> map.toString())
.collect(Collectors.toList());
// 결과 확인
log.info(response.toString());
}
}
@SpringBootTest
class WebClientApplicationTests {
@Autowired
private WebClientServiceImpl webClientService;
@Test
void getFlux() {
webClientService.getFlux();
}
}
INFO 40948 --- [ Test worker] c.w.service.WebClientServiceImpl : [{code=myCode, message=Success}]