WebClient는 다양한 메서드 체인을 통해 웹 리소스에 접근하여 데이터를 가져오거나 전송할 수 있다.
WebClient
의 사용 이유에 대해서 간단히 알아보겠다.
HTTP 요청 및 응답 처리 간소화
: WebClient를 사용하면 HTTP 요청을 보내고 응답을 받는 과정이 간단해진다. URL을 기반으로 요청을 생성하고 데이터를 가져오거나 전송하는 작업을 더 쉽게 수행할 수 있다.
비동기 및 논블로킹 지원
: Spring WebFlux와 함께 사용할 때, WebClient는 비동기 및 논블로킹 프로그래밍을 지원한다. 이는 대용량 트래픽과 병렬 처리에 유용하며, 블로킹되지 않고 다양한 요청을 동시에 처리할 수 있다.
클라이언트 커스터마이징
: WebClient.Builder를 사용하여 클라이언트를 커스터마이징할 수 있습니다. 다양한 옵션을 설정하여 요청 헤더, 쿠키, 기본 URL 등을 관리하고, 필터 및 전략을 통해 요청 및 응답을 추가로 처리할 수 있다.
RESTful API와의 통합
: 대부분의 RESTful API는 HTTP를 통해 데이터를 제공하거나 받는다. WebClient를 사용하면 서버의 API 엔드포인트와 통신하는 것이 간편해지며, 요청 및 응답을 자동으로 직렬화 및 역직렬화하여 처리할 수 있다.
*외부 서비스와의 통합!!!
: 다른 웹 서비스나 외부 API와 통합해야 할 때 WebClient를 사용하면 데이터를 가져오고 전송하기 위한 중요한 기능을 편리하게 제공받을 수 있다.
테스트 용이성
: WebClient를 사용하면 Mock 서버 또는 테스트 환경에서 웹 리소스와의 상호 작용을 테스트하는 것이 쉬워진다. 단위 테스트와 통합 테스트에서 사용될 수 있다.
다양한 데이터 형식 지원
: WebClient는 JSON, XML 등 다양한 데이터 형식과 함께 작동할 수 있다. 또한 이미지, 파일 등 다양한 데이터 형태를 다운로드하거나 업로드할 수 있다.
😀 WebClient
를 활용하여 API 연동하기
WebClient를 사용하기 위해서는 우선 dependencies
를 추가해 주어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
또한 WebClient 빈 설정을 하여 @Configuration
을 통해 WebClient.builder 빈을 등록하고 필요한 옵션을 설정할 수 있다.
WebClient의 인스턴스 생성방법에는 간단하게 바로 인스턴스를 생성하는 방법과 builder를 사용하는 방법 두가지가 있다.
// 기본 설정으로 생성
WebClient webclient = WebClient.create();
// base url을 추가하여 생성
WebClient webclient = WebClient.create("http://localhost:8080);
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8080")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
⚽ WebClient.build()
: create()가 default 설정이라면, build()를 이용해 커스텀하게 설정을 변경할 수 있다.
💡 *WebClient 는 기본적으로 Immutable 하게 생성되므로 **싱클톤**으로 객체를 생성해서 설정을 그때그때 변경해서 사용할려면 mutable() 속성을 사용하여 생성할 수 있습니다.*
builder에 들어갈 수 있는 옵션
uriBuilderFactory
: Customized UriBuilderFactory to use as a base URL.
defaultUriVariables
: default values to use when expanding URI templates.
defaultHeader
: Headers for every request.
defaultCookie
: Cookies for every request.
defaultRequest
: Consumer to customize every request.
filter
: Client filter for every request.
exchangeStrategies
: HTTP message reader/writer customizations.
clientConnector
: HTTP client library settings.
(WebClient를 사용해서) API를 사용하기 위한 인증key를 사용하기 위해 인코딩을 하지 않아 API키가 달라지는 경우 존재.
// uribuild 설정을 도와주는 DefaultUriBUilderFactory 호출
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
// 인코딩 모드 설정
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
인코딩 모드 종류는 다음과 같다
TEMPLATE_AND_VALUES
: URI 템플릿을 먼저 인코딩 하고 URI 변수를 적용할 때 인코딩한다.VALUES_ONLY
: URI 템플릿을 인코딩하지 않고 URI 변수를 템플릿에 적용하기 전에 엄격히 인코딩한다.URI_COMPONENT
: URI 변수를 적용한 후에 URI 컴포넌트를 인코딩한다.NONE
: 인코딩을 적용하지 않는다.이렇게 URI의 빌드 설정을 완료했다면 이제 WebClient의 인스턴스를 생성할 때 UriBuilder로 Uri를 만든다고 설정한다.
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
⚽ baseUrl(String baseUrl)
:
기본 URL을 설정합니다. 모든 상대 URL은 이 기본 URL을 기준으로 합쳐집니다.
예: .baseUrl("https://api.example.com")
⚽ defaultHeader(String headerName, String... headerValues)
:
모든 요청에 기본 헤더를 추가합니다.
예: .defaultHeader("Authorization", "Bearer your_token")
⚽ defaultCookie(String cookieName, String... cookieValues)
:
모든 요청에 기본 쿠키를 추가합니다.
예: .defaultCookie("session_id", "12345")
⚽ defaultUriVariables(Map<String, ?> defaultUriVariables)
:
URI 템플릿 확장 시 사용할 기본 변수를 설정합니다.
예: .defaultUriVariables(Collections.singletonMap("apiKey", "your_api_key"))
⚽ filter(ExchangeFilterFunction filter)
:
클라이언트 필터를 추가합니다. 요청 및 응답을 수정하거나 가로챌 수 있습니다.
예: .filter((request, next) -> next.exchange(request))
⚽ exchangeStrategies(ExchangeStrategies strategies)
:
HTTP 메시지 리더 및 라이터를 설정합니다. 데이터 직렬화 및 역직렬화에 사용됩니다.
예: .exchangeStrategies(ExchangeStrategies.builder().codecs(configureCodecs).build())
⚽ clientConnector(ClientHttpConnector clientConnector)
:
HTTP 클라이언트 커넥터를 설정합니다. HTTP 클라이언트 라이브러리의 설정을 조정할 수 있습니다.
예: .clientConnector(new ReactorClientHttpConnector())
⚽ build()
:
모든 설정이 완료되면 WebClient 인스턴스를 빌드하고 반환합니다.
WebClient.Builder를 사용하여 WebClient 인스턴스를 생성하면 위의 메서드 체인들을 활용하여 클라이언트를 원하는 방식으로 커스터마이징할 수 있습니다.
각각의 메서드는 클라이언트의 동작을 변경하거나 설정하기 위한 것으로, 필요에 따라 적절한 옵션을 사용하여 웹 요청 및 응답 처리를 조정할 수 있습니다.
⚽ get()
:
GET 요청을 생성합니다.
post()
:
POST 요청을 생성합니다.
put()
:
PUT 요청을 생성합니다.
delete()
:
DELETE 요청을 생성합니다.
head()
:
HEAD 요청을 생성합니다.
options()
:
OPTIONS 요청을 생성합니다.
method(HttpMethod method)
:
주어진 HttpMethod으로 사용자 정의 HTTP 요청을 생성합니다.
uri(String uriTemplate, Object... uriVariables)
:
URI 템플릿을 확장하고 파라미터를 주입하여 요청 URI를 구성합니다.
header(String headerName, String... headerValues)
:
HTTP 요청 헤더를 추가합니다.
uri(URI uri)
:
주어진 URI를 사용하여 요청을 생성합니다.
accept(MediaType... acceptableMediaTypes)
:
Accept 헤더를 설정합니다.
contentType(MediaType contentType)
:
Content-Type 헤더를 설정합니다.
body(BodyInserter<?, ? super ClientHttpRequest> inserter)
:
요청 바디 데이터를 설정합니다.
retrieve()
:
요청을 보내고 응답을 수신합니다.
exchange()
:
요청을 보내고 응답을 ClientResponse 객체로 수신합니다.
mutate()
:
현재 WebClient 인스턴스를 복제하여 수정 가능한 빌더를 반환합니다.
이 메서드 체인들을 조합하여 원하는 기능과 동작을 구현하고, 웹 서비스와 상호 작용하는 작업을 수행할 수 있습니다.
이제 위에서 인스턴스를 생성했으니 응답값을 요청하는 걸 알아보자.
소스코드를 보면서 분석하자.
//미리 생성된 WebClient의 인스턴스 webClient
String response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("serviceKey", serviceKey)
.queryParam("upkind", upkind)
.queryParam("upr_cd", upr_cd)
.queryParam("state", state)
.queryParam("pageNo", pageNo)
.queryParam("NumOfRows", NumOfRows)
.queryParam("_type", _type)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
🖤 가장 먼저 어떤 HTTP 메서드를 통해 요청을 보낼 지를 결정.. 위 코드는 GET 방식으로 요청을 보낸다.
webClient.get()
: 내가 사용하려는 API의 문서를 보고 GET방식을 사용한다면. 요청방식을 get으로 해주기!!uri(uriBuilder → uriBuilder …)
: 스트림 부분. baseURI 및 파라미터를 지정해주는 부분이다. 위에서 이미 baseURI를 지정했다면 안써도됨. + 그 뒤에 파라미터들을 uriBuilder로 build해주자retrieve()
: 응답값을 받게 해주는 메소드이다.bodyToMono(String.class)
: response body를 String 타입으로 받게 해준다.block()
: WebClient는 기본적으로 비동기 방식인데, block메서드를 이용해 동기 방식으로 바꿔준다. block을 붙여줘야 string으로 바꿀 수 있다고 한다.++ mono와 flux의 개념을 알기 위해선 Spring WebFlux를 공부해야 한다.
해당 링크를 통해 공부하기.
Java 1.8 샘플 코드를 WebClient로 변환한 부분!
/* Java 1.8 샘플 코드 */
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.io.BufferedReader;
import java.io.IOException;
public class ApiExplorer {
public static void main(String[] args) throws IOException {
StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/1543061/abandonmentPublicSrvc/abandonmentPublic"); /*URL*/
urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=서비스키"); /*Service Key*/
urlBuilder.append("&" + URLEncoder.encode("bgnde","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*유기날짜(검색 시작일) (YYYYMMDD)*/
urlBuilder.append("&" + URLEncoder.encode("endde","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*유기날짜(검색 종료일) (YYYYMMDD)*/
urlBuilder.append("&" + URLEncoder.encode("upkind","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*축종코드 (개 : 417000, 고양이 : 422400, 기타 : 429900)*/
urlBuilder.append("&" + URLEncoder.encode("kind","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*품종코드 (품종 조회 OPEN API 참조)*/
urlBuilder.append("&" + URLEncoder.encode("upr_cd","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*시도코드 (시도 조회 OPEN API 참조)*/
urlBuilder.append("&" + URLEncoder.encode("org_cd","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*시군구코드 (시군구 조회 OPEN API 참조)*/
urlBuilder.append("&" + URLEncoder.encode("care_reg_no","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*보호소번호 (보호소 조회 OPEN API 참조)*/
urlBuilder.append("&" + URLEncoder.encode("state","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*상태(전체 : null(빈값), 공고중 : notice, 보호중 : protect)*/
urlBuilder.append("&" + URLEncoder.encode("neuter_yn","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*상태 (전체 : null(빈값), 예 : Y, 아니오 : N, 미상 : U)*/
urlBuilder.append("&" + URLEncoder.encode("pageNo","UTF-8") + "=" + URLEncoder.encode("1", "UTF-8")); /*페이지 번호 (기본값 : 1)*/
urlBuilder.append("&" + URLEncoder.encode("numOfRows","UTF-8") + "=" + URLEncoder.encode("10", "UTF-8")); /*페이지당 보여줄 개수 (1,000 이하), 기본값 : 10*/
urlBuilder.append("&" + URLEncoder.encode("_type","UTF-8") + "=" + URLEncoder.encode(" ", "UTF-8")); /*xml(기본값) 또는 json*/
URL url = new URL(urlBuilder.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-type", "application/json");
System.out.println("Response code: " + conn.getResponseCode());
BufferedReader rd;
if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
StringBuilder sb = new StringBuilder();
String line;
while ((line = rd.readLine()) != null) {
sb.append(line);
}
rd.close();
conn.disconnect();
System.out.println(sb.toString());
}
}
위 부분을 WebClient를 사용해서 api를 호출하게 된다면 !!
private final static String BASE_URL = "http://apis.data.go.kr/1543061/abandonmentPublicSrvc/abandonmentPublic";
private final String API_KEY = "Y8ysgjcfITdLKYzk9pQp6pzphI2yY95czKzFUggqOQCdYuYLm9oAOBh%2Fhn1meZKp1UPtONWLAAIbu7McjP9R9Q%3D%3D";
@RequestMapping(value = "/webclient-test", produces = "application/json; charset=utf8")
public String getAbandonedAnimal(Model model) {
String serviceKey = API_KEY;
String upkind = "422400";
String upr_cd = "6110000";
String state = "notice";
String pageNo = "1";
String NumOfRows = "8";
String _type = "json";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(BASE_URL);
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);
WebClient webClient = WebClient.builder()
.uriBuilderFactory(factory)
.baseUrl(BASE_URL)
.build();
Mono<Map<Object, Object>> response = webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("serviceKey", serviceKey)
.queryParam("upkind", upkind)
.queryParam("upr_cd", upr_cd)
.queryParam("state", state)
.queryParam("pageNo", pageNo)
.queryParam("NumOfRows", NumOfRows)
.queryParam("_type", _type)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
model.addAttribute("response", response);
return "test/test";
}
⚽ retrvive()
: retrive() 메소드는 ClientResponse 개체의 body를 받아 디코딩하고 사용자가 사용할 수 있도록 미리 만든 개체를 제공하는 간단한 메소드 입니다.
⚽ exchange()
: ClientResponse를 상태값, 헤더와 함께 가져오는 메소드입니다.
➡️ retrive() 는 예외를 처리하기 좋은 api 를 가지고 있지않아, 응답 상태 및 헤더와 함게 더 세밀한 조정이 하고싶을 떄는 exchange를 사용하라고 합니다.
➡️ 하지만, exchange()를 통해 세밀한 조정이 가능하지만, Response 컨텐츠에 대해 모든 처리를 직접 하게되면 메모리 누수가 발생할 가능성이 존재하기 때문에 retrive()를 권장한다고 합니다.
결국 retrieve() 사용하기 !
알리고에서는 POST로 formData에 값을 넣어서 요청하면, JSON 형태로 응답값을 준다고 API 스펙에 명시되어 있다.
JSON 바디값을 객체로 받아오기 위해서 생성자나 Setter가 필요하진 않다.
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();
}