Framework/Spring

[Spring Boot]HttpClient와 WebClient 사용해보기

  • -
반응형

오랜만에 외부 API 연동을 하게 되어 기존에 사용중인 HttpClient 방식이 아닌 WebClient를 사용해보기로 했습니다. 

오늘은 HttpClient, WebClient 각각의 사용 예제를 알아보겠습니다.

 

※ 테스트 환경
  • Spring Boot 2.7
  • JDK 11

WebClient는 Spring 5부터 등장한 HTTP 클라이언트 라이브러리입니다.

WebClient는 다음과 같은 특징이 있습니다.

  • Spring Boot 지원: WebClient는 Spring WebFlux의 일부로, Spring Boot와 완벽히 통합됩니다.
  • 비동기 처리: WebClient는 기본적으로 비동기(reactive)이며, 동기 처리도 지원합니다.
  • 간결한 코드: 요청/응답 처리 코드가 간결하며, JSON 직렬화/역직렬화가 기본 지원됩니다.
  • 확장성: 기본 설정 외에도 ExchangeFilterFunction을 통해 요청/응답 로깅, 인증 추가 등 쉽게 확장 가능합니다.
  • JDK 11과 호환성: 내부적으로 JDK 11의 HTTP 클라이언트를 활용하며, 성능 최적화가 이루어집니다.

 

HttpClient 사용 방법부터 살펴보겠습니다.

예제에는 하루단어에 사용되는 API를 사용했습니다. API 호출시 단어 리스트를 응답받을 수 있습니다. 사용된 DTO, Controller는 다음과 같습니다.

import lombok.Data;

@Data
public class WordDto {

    private Long wordSeq;
    private String wordName;
    private String exampleName;
    private String wordMean;
    private String pronounce;
}
import com.app.demo.common.components.WebClientApi;
import com.app.demo.common.dto.MessageDto;
import com.app.demo.domain.web.rest.dto.WordDto;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class TestApiController {

    private final WebClientApi webClientApi;
    
    @GetMapping("/test")
    public ResponseEntity<MessageDto> getTestCompanyList() {
        List<WordDto> wordDtoList = webClientApi.getWordList();
        return ResponseEntity.ok().body(new MessageDto("success", wordDtoList));
    }
}

 

Component로 생성된 WebClientApi에서는 코드드림 API를 호출합니다.

import com.app.demo.domain.web.rest.dto.WordDto;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.json.simple.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.client.WebClient;

@Slf4j
@Component
public class WebClientApi {

    private static final String TEST_API_URL = "API 호출 주소";
    private static final String CLIENT_SECRET = "API시크릿키";
    
    public List<WordDto> getWordList() {
        List<WordDto> companyList = new ArrayList<>();

        try {
            Map<String, Object> paramMap = new HashMap<>();
            paramMap.put("searchText", "");
            // 1. HttpClient
            JSONObject jsonObject = callHttpClientApi("/word", paramMap);            
            // 2. WebClient
            // JSONObject jsonObject = callWebClientApi("/word", paramMap);
            
            if (jsonObject != null) {
                ObjectMapper objectMapper = new ObjectMapper();

                if ("success".equals(String.valueOf(jsonObject.get("message")))) {
                    companyList = objectMapper.convertValue(jsonObject.get("result"),
                        new TypeReference<>() {
                        });
                }
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return companyList;
    }

}

 

HttpClient

JDK 11부터 기본적으로 제공되는 HttpClient는 모던한 HTTP/2 프로토콜을 지원하며, 고성능 네트워크 처리가 가능합니다.

 

[GET]

호출 주소와 Map 파라미터를 받는 callHttpClientApi 메소드입니다. 

GET 방식으로 호출시 쿼리 파라미터를 주소뒤에 추가할 수 있도록 paramMap를 String 형태로 변환하여 uri를 생성하게 됩니다. 문자열로 응답 받은 결과는 JSONObject로 변환하여 반환합니다.

private JSONObject callHttpClientApi(
    String callUrl,
    Map<String, Object> paramMap
) {
    JSONObject jsonObject = null;

    try {
        String query = paramMap.entrySet().stream()
            .map(entry ->
                URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "=" +
                    URLEncoder.encode(String.valueOf(entry.getValue()), StandardCharsets.UTF_8))
            .collect(Collectors.joining("&"));

        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(TEST_API_URL + callUrl + "?" + query))
            .header("Content-Type", "application/json")
            .header("CLIENT-KEY", CLIENT_SECRET)
            .GET()
            .build();
        HttpResponse<String> response = client.send(request,
            HttpResponse.BodyHandlers.ofString());

        log.info(response.body());

        ObjectMapper objectMapper = new ObjectMapper();
        jsonObject = objectMapper.readValue(response.body(), JSONObject.class);
    } catch (Exception e) {
        log.error(e.getMessage());
    }

    return jsonObject;
}

 

[POST]

POST 방식으로 호출시에는 다음과 같이 변경할 수 있습니다. ObjectMapper를 이용해 Map을 JSON 형식으로 변환하여 BodyPublishers.ofString에 담아 전송합니다.

ObjectMapper objectMapper = new ObjectMapper();
String paramJson = objectMapper.writeValueAsString(paramMap);

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(TEST_API_URL + callUrl))
    .header("Content-Type", "application/json")
    .header("CLIENT-KEY", CLIENT_SECRET)
    .POST(BodyPublishers.ofString(paramJson))
    .build();
HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());

log.info(response.body());

 

API를 호출해보면 결과값을 확인할 수 있습니다.

 

 

WebClient

WebClient를 사용한 방식을 살펴보겠습니다.

 

[GET]

HttpClient를 사용한 메소드와 동일한 인자값을 갖는 callWebClientApi 메소드를 생성했습니다.

paramMap에 있는 데이터를 MultiValueMap으로 변환하여 쿼리 파라미터로 추가합니다. 응답을 Mono<String>으로 변환하여 결과값을 JSONObject로 반환합니다.

 

private JSONObject callWebClientApi(
    String callUrl,
    Map<String, Object> paramMap
) {
    JSONObject jsonObject = null;

    try {
        WebClient webClient = WebClient
            .builder()
            .baseUrl(TEST_API_URL)
            .defaultHeader("Content-Type", "application/json")
            .defaultHeader("CLIENT-KEY", CLIENT_SECRET)
            .build();

        MultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
        paramMap.forEach((key, value) -> multiValueMap.add(key, String.valueOf(value)));

        String response = webClient
            .get()
            .uri(uriBuilder -> uriBuilder.path(callUrl)
                .queryParams(multiValueMap)
                .build())
            .retrieve()
            .bodyToMono(String.class)
            .block();

        ObjectMapper objectMapper = new ObjectMapper();
        jsonObject = objectMapper.readValue(response, JSONObject.class);
    } catch (Exception e) {
        log.error(e.getMessage());
    }

    return jsonObject;
}

 

[POST]

POST 방식으로 호출할 때에는 다음과 같이 변경할 수 있습니다.

변환 과정없이 paramMap을 bodyValue에 추가합니다.

WebClient webClient = WebClient
    .builder()
    .baseUrl(TEST_API_URL)
    .defaultHeader("Content-Type", "application/json")
    .defaultHeader("CLIENT-KEY", CLIENT_SECRET)
    .build();

String response = webClient.post()
    .uri(callUrl)
    .bodyValue(paramMap)
    .retrieve()
    .bodyToMono(String.class)
    .block();

 

 

WebClient 사용시 다음과 같은 에러 메세지를 받을 수 있습니다.

이는 WebFlux 기본으로 설정된 코덱 메모리 사이즈인 256KB를 초과하여 발생한 에러입니다. 

nested exception is org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

 

WebClient 빌드시에 maxInMemorySize 설정을 통해 이를 해결할 수 있습니다.

WebClient webClient = WebClient
	.builder()
    .baseUrl(API_URL)
    // 1MB 설정
    .codecs(configure -> configure.defaultCodecs().maxInMemorySize(1024 * 1024)
    .build();

 

반응형
Contents

포스팅 주소를 복사했습니다.

이 글이 도움이 되었다면 공감 부탁드립니다.