WebClient - json

Chooooo·2023년 8월 29일
0

TIL

목록 보기
2/28
post-thumbnail

😎 WebClient

WebClient는 다양한 메서드 체인을 통해 웹 리소스에 접근하여 데이터를 가져오거나 전송할 수 있다.
WebClient의 사용 이유에 대해서 간단히 알아보겠다.

  1. HTTP 요청 및 응답 처리 간소화: WebClient를 사용하면 HTTP 요청을 보내고 응답을 받는 과정이 간단해진다. URL을 기반으로 요청을 생성하고 데이터를 가져오거나 전송하는 작업을 더 쉽게 수행할 수 있다.

  2. 비동기 및 논블로킹 지원: Spring WebFlux와 함께 사용할 때, WebClient는 비동기 및 논블로킹 프로그래밍을 지원한다. 이는 대용량 트래픽과 병렬 처리에 유용하며, 블로킹되지 않고 다양한 요청을 동시에 처리할 수 있다.

  3. 클라이언트 커스터마이징: WebClient.Builder를 사용하여 클라이언트를 커스터마이징할 수 있습니다. 다양한 옵션을 설정하여 요청 헤더, 쿠키, 기본 URL 등을 관리하고, 필터 및 전략을 통해 요청 및 응답을 추가로 처리할 수 있다.

  4. RESTful API와의 통합: 대부분의 RESTful API는 HTTP를 통해 데이터를 제공하거나 받는다. WebClient를 사용하면 서버의 API 엔드포인트와 통신하는 것이 간편해지며, 요청 및 응답을 자동으로 직렬화 및 역직렬화하여 처리할 수 있다.

  5. *외부 서비스와의 통합!!!: 다른 웹 서비스나 외부 API와 통합해야 할 때 WebClient를 사용하면 데이터를 가져오고 전송하기 위한 중요한 기능을 편리하게 제공받을 수 있다.

  • Open API를 사용하는 경우에 활용
  1. 테스트 용이성: WebClient를 사용하면 Mock 서버 또는 테스트 환경에서 웹 리소스와의 상호 작용을 테스트하는 것이 쉬워진다. 단위 테스트와 통합 테스트에서 사용될 수 있다.

  2. 다양한 데이터 형식 지원: WebClient는 JSON, XML 등 다양한 데이터 형식과 함께 작동할 수 있다. 또한 이미지, 파일 등 다양한 데이터 형태를 다운로드하거나 업로드할 수 있다.

😀 WebClient를 활용하여 API 연동하기

  • 먼저 webflux 의존성 추가

👻 적용하기

의존성 추가

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);
  • builder를 사용하는 방법
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.

    • UriBuilderFactory는 URI를 생성하기 위한 팩토리 클래스. 기본 UriBuilderFactory를 커스터마이징하여 원하는 방식으로 URI를 생성할 수 있다.
  • defaultUriVariables: default values to use when expanding URI templates.

    • URI 템플릿 확장 시 사용할 기본 값들을 설정할 수 있다. URI에 변수가 포함된 경우 이를 확장할 때 기본 값이 사용된다.
  • defaultHeader: Headers for every request.

    • 모든 요청에 대해 사용될 기본 헤더를 설정할 수 있다. 보통 사용자 에이전트, 인증 등을 여기에 추가한다.
  • defaultCookie: Cookies for every request.

    • 모든 요청에 대해 사용될 기본 쿠키를 설정할 수 있다. 세션 관리 등에 사용될 수 있다.
  • defaultRequest: Consumer to customize every request.

    • 모든 요청에 대해 커스텀 설정을 적용할 수 있는 Consumer를 제공한다. 요청 시마다 호출되며 요청을 커스터마이징할 수 있다.
  • filter: Client filter for every request.

    • 모든 요청에 대해 적용되는 클라이언트 필터를 설정할 수 있다. 요청 전후에 추가적인 로직을 수행하거나 요청/응답을 가로챌 수 있다.
  • exchangeStrategies: HTTP message reader/writer customizations.

    • HTTP 메시지의 리더와 라이터를 커스터마이징할 수 있는 전략을 설정한다. 데이터 직렬화 및 역직렬화에 사용.
  • clientConnector: HTTP client library settings.

    • HTTP 클라이언트 라이브러리의 설정을 커스터마이징할 수 있다. 연결 시간 제한, 풀링 설정 등을 조정할 수 있다.

(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();

😀 WebClient.builder()의 다양한 메서드 체인들

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 인스턴스를 생성하면 위의 메서드 체인들을 활용하여 클라이언트를 원하는 방식으로 커스터마이징할 수 있습니다.
각각의 메서드는 클라이언트의 동작을 변경하거나 설정하기 위한 것으로, 필요에 따라 적절한 옵션을 사용하여 웹 요청 및 응답 처리를 조정할 수 있습니다.

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를 공부해야 한다.

[mono & flux]

해당 링크를 통해 공부하기.


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";								
	}

😀 WebClient Request / Response 핸들링하기

  • GET, POST, PUT, DELETE 모두 사용가능하고 모두 비슷.
  • WebClient를 사용하여 응답을 받는 방식으로 retrieve()와 exchange() 2가지 메소드를 이용할 수 있다.

retrieve() vs exchange()

retrvive() : retrive() 메소드는 ClientResponse 개체의 body를 받아 디코딩하고 사용자가 사용할 수 있도록 미리 만든 개체를 제공하는 간단한 메소드 입니다.

exchange(): ClientResponse를 상태값, 헤더와 함께 가져오는 메소드입니다.

➡️ retrive() 는 예외를 처리하기 좋은 api 를 가지고 있지않아, 응답 상태 및 헤더와 함게 더 세밀한 조정이 하고싶을 떄는 exchange를 사용하라고 합니다.

➡️ 하지만, exchange()를 통해 세밀한 조정이 가능하지만, Response 컨텐츠에 대해 모든 처리를 직접 하게되면 메모리 누수가 발생할 가능성이 존재하기 때문에 retrive()를 권장한다고 합니다.

결국 retrieve() 사용하기 !

retrieve()

  • retrive() 를 사용할 때는, toEntity(), bodyToMono(), bodyToFlux() 이렇게 response를 받아올 수 있습니다.
  • bodyToFlux, bodyToMono 는 가져온 body를 각각 Reactor의 Flux와 Mono 객체로 바꿔줍니다.
    • 헤더를 제외한 Body값만 가져올 때 사용 ?!
  • Block() 을 이용해서 Mono 나 Flux가 아닌 동기식 스타일 일반 객체로 받을 수도 있습니다.

API 사용하면서 적용

  • 알리고 토큰 생성 API 사용 예제

알리고에서는 POST로 formData에 값을 넣어서 요청하면, JSON 형태로 응답값을 준다고 API 스펙에 명시되어 있다.

JSON 바디값을 객체로 받아오기 위해서 생성자나 Setter가 필요하진 않다.

  • 그 이유는 bodyToMono(class 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();
}
profile
back-end, 지속 성장 가능한 개발자를 향하여

0개의 댓글