Spring WebClient를 활용하여 HTTP 요청 처리하기

WebClient란?

WebClient가 도입되기 전에는 주로 RestTemplate을 사용하여 HTTP 요청을 보냈었습니다. RestTemplate은 Spring의 기본적인 HTTP 클라이언트 라이브러리로, 동기적으로 동작합니다. 즉, RestTemplate을 사용하면, 요청과 응답을 처리하기 위해 Blocking I/O 방식을 사용하여 스레드를 블로킹하고 기다립니다. 이는 스레드 자원의 낭비를 초래할 수 있고, 이로 인해 서버의 응답성이 저하될 수 있습니다. `WebClient`는 RestTemplate의 단점을 개선하기 위해 비동기적인 방식으로 동작하여, 동시에 여러 요청을 처리할 수 있습니다. 또한 리액티브 프로그래밍과 함께 사용하기에 적합합니다.

 

  1. 비동기 처리 (동기적인 처리도 가능)
  2. 리액티브 프로그래밍에 적합 

 

💁‍♂️ HTTP 요청을 보내는 방법 

`HttpURLConnection`:
JDK에 내장되어 있는 Java의 기본적인 HTTP 클라이언트 라이브러리입니다. 동기적인 방식으로 동작하여, 별도의 스레드 관리가 필요하고, 기능이 제한적입니다.

`RestTemplate`:
Spring의 HTTP 클라이언트 라이브러로, 동기적으로 동작합니다. 기본적인 기능은 편리하게 사용할 수 있지만, 별도의 스레드 관리가 필요합니다.

`HttpClient`: 
Apache에서 제공하는 HTTP 클라이언트 라이브러리입니다. 위 방식들보다 다양한 기능과 커스터마이징이 가능하여 더 복잡한 HTTP 요청 처리가 가능합니다. 동기적인 방식으로 동작하여, 별도의 비동기 처리를 위해선 스레드 관리가 필요합니다.    

`WebClient`:
Spring WebFlux에서 제공하는 비동기 HTTP 클라이언트입니다. 리액티브 프로그래밍을 지원하고, 논 블로킹 I/O 모델로 동작하여 높은 확장성과 성능을 제공합니다. 비동기적인 방식으로 동작하기 때문에 다수의 요청을 동시에 처리하는데 효율적입니다. 

✏️Spring Framework를 사용하는 프로젝트라면 `RestTemplate`을 사용하여 간편하고 일관성 있는 개발. 복잡한 커스터마이징이 필요한 경우 `HttpClient` 사용. 리액티브 프로그래밍과 논 블로킹 I/O를 지원하는경우는 WebClient를 활용하는 것을 고려해보면 됩니다.

✏️WebClient는 비동기적인 방식으로 동작하는HTTP 클라이언트지만,  동기적인 HTTP 요청을 처리할 수 있는 메서드(.block(), .blockOptional())를 제공합니다. 이 메서드를 활용하면 요청을 동기적으로 처리가 가능합니다. 

WebClient 사용 방법

1. 의존성 추가 및 Bean 등록

WebClient를 의존성 추가해주고 빈으로 등록해줍니다.

 

gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

 

java 메인 클래스:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

@SpringBootApplication
public class MainApplication {

    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }

    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }

}

 

2. GET 요청 보내기

기본 코드:

    private final WebClient webClient;

    public WebClientUtil(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.build();
    }

    public <T> Mono<T> get(String baseUrl, String url, Class<T> responseType) {
        return webClient.get()
                .uri(baseUrl + url)        // API 엔드 포인트
                .retrieve()                // 요청 보내기
                .bodyToMono(responseType); // 응답 데이터를 Mono<T>로 변환
    }
  • `WebClient`를 사용하여 HTTP 요청을 생성합니다.
  • `uri()` 메서드를 사용하여 요청의 URI를 설정합니다.
  • `retrieve()` 메서드를 사용하여 요청을 보냅니다.
  • `bodyToMono()` 메서드를 사용하여 응답 데이터를 Mono로 변환합니다.

 

 

파라미터 포함:

파라미터가 존재하지 않는 경우에도 정상 작동. 

public <T> Mono<T> get(String baseUrl, String url, MultiValueMap<String, String> params, Class<T> responseType) {
    UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(baseUrl + url)
            .queryParams(params);

    URI uri = uriBuilder.build().toUri();

    return webClient.get()
            .uri(uri)
            .retrieve()
            .bodyToMono(responseType);
}

 

응답 데이터 처리 방법:

String baseUrl = "http://example.com";

// GET 요청 후 응답 처리
Mono<MyResponse> response = webClientUtil.get(baseUrl, "/path", MyResponse.class);
response.subscribe(
    data ->  {System.out.println("GET 요청 성공 - 응답 데이터: " + data);},
    error -> {System.err.println("GET 요청 실패 - 오류 발생: " + error.getMessage());},
    () ->    {System.out.println("GET 요청 완료"); }
);

// param
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("id", "1");
params.add("id", "2");
params.add("id", "3");


// GET 요청 후 JSON 응답 처리
Mono<MyResponse> jsonResponse = webClientUtil.get(baseUrl, "/json-path", params, MyResponse.class);
jsonResponse.subscribe(response -> {
    System.out.println("JSON 응답 데이터: " + response);
});

// GET 요청 후 XML 응답 처리
Mono<String> xmlResponse = webClientUtil.get(baseUrl, "/xml-data", String.class);
xmlResponse.subscribe(response -> {
    System.out.println("XML 응답 데이터: " + response);
});

// GET 요청 후 문자열 응답 처리
Mono<String> stringResponse = webClientUtil.get(baseUrl, "/string-data", String.class);
stringResponse.subscribe(response -> {
    System.out.println("문자열 응답 데이터: " + response);
});
  • `subscribe()`:
    일반적으로 GET 요청 후 응답 처리 방법은 `subscribe()` 메서드를 사용하여 응답 데이터를 처리합니다. 이 메서드는 비동기로 응답 데이터가 도착했을 때 처리할 콜백을 등록하는역할을합니다.
    `subscribe()`의 첫 번째 인자는 응답 데이터를 처리하는 콜백을 전달하고, 두 번째 인자로는 오류가 발생했을 때 처리하는 콜백을, 세 번째 인자는 응답처리가 완료되었을 때 호출되는 콜백입니다.
  • 응답 데이터 처리 방법:
    객체로 응답되면 객체로, 배열로 응답되면 배열로, 문자열로 응답되면 문자열로 처리해주면 됩니다. JSON으로 응답될 때는 객체로, XML로 응답될 때는 String으로 받아주면 됩니다.

 

3. POST 요청 보내기

post 메서드:

입맛대로 커스텀하면 됩니다. 

public <T> Mono<T> post(String baseUrl, String url, Object request, MediaType mediaType, Class<T> responseType, Map<String, String> headers) {
    return webClient.post()
            .uri(baseUrl + url)
            .contentType(mediaType)
            .headers(httpHeaders -> httpHeaders.addAll(headers))
            .bodyValue(request)
            .retrieve()
            .bodyToMono(responseType);
}

 

    post 호출:
// 요청 데이터 객체 생성
MyRequest request = new MyRequest();
request.setName("John");
request.setAge(30);

// 요청 헤더 정보 설정 (예시로 빈 맵을 전달)
Map<String, String> headers = new HashMap<>();

// POST 요청 보내기
Mono<MyResponse> jsonResponse = webClientUtil.post("http://example.com", "/path", request, MediaType.APPLICATION_JSON, MyResponse.class, headers);

// 응답 데이터 처리
jsonResponse.subscribe(response -> {
    System.out.println("JSON 응답 데이터: " + response);
});

`contentType()`:
적절한 `MediaType` 객체와 함께 호출하여 요청 본문 형식을 지정해주면 됩니다. `bodyValue()` 메서드를 활용해 `MediaType`과 맞는 형식의 데이터를 요청 본문에 담아 보내면 됩니다. 

  • MediaType.APPLICATION_JSON: JSON 데이터 요청 본문에 담기
  • MediaType.APPLICATION_FORM_URLENCODED: 폼 데이터 요청 본문에 담기
  • MediaType.APPLICATION_XML: XML 데이텨 요청 본문에 담기

 

4. PUT, DELETE 요청 보내기

put, delete 메서드:

추가적으로 커스텀하고 싶으면 위를 참고하면 됩니다.

public <T> Mono<T> put(String baseUrl, String url, Object request, Class<T> responseType) {
    return webClient.put()
            .uri(baseUrl + url)
            .bodyValue(request)
            .retrieve()
            .bodyToMono(responseType);
}

public Mono<Void> delete(String baseUrl, String url) {
    return webClient.delete()
            .uri(baseUrl + url)
            .retrieve()
            .bodyToMono(Void.class);
}

 

put, delete 요청:

// PUT 요청
Mono<MyResponse> putResponse = webClientUtil.put(baseUrl, "/path/1", requestBody, MyResponse.class);

// DELETE 요청
Mono<Void> deleteResponse = webClientUtil.delete(baseUrl, "/path/1");

// 결과 처리
putResponse.subscribe(response -> System.out.println("PUT 응답: " + response));
deleteResponse.subscribe(() -> System.out.println("DELETE 성공"));

 

5. 응답의 에러 및 오류 처리

`doOnSuccess` & `doOnError` :

    public <T> Mono<T> get(String baseUrl, String url, Class<T> responseType) {
        return webClient.get()
                .uri(baseUrl + url)
                .retrieve()
                .bodyToMono(responseType)
                .doOnSuccess(response -> System.out.println("응답 데이터: " + response))
                .doOnError(error -> System.err.println("오류 발생: " + error));
    }

`doOnSuccess`와 `doOnError` 메서드는 요청을 보낸후 성공적으로 응답을 받았을 때와 오류가 발생했을 때 실행될 될동작을 지정합니다. doOnSuccess와 doOnError는 Mono나 Flux의 연산자로서, 비동기적인 응답 처리와 관련하여 다른 로직을 수행하기 위해 사용됩니다.

 

`onStatus`:

public <T> Mono<T> get(String url, Class<T> responseType) {
    return webClient.get()
            .uri(url)
            .retrieve()
            .onStatus(status -> status.equals(HttpStatus.NOT_FOUND), this::handleNotFoundError)
            .onStatus(status -> status.isError(), this::handleErrorResponse)
            .bodyToMono(responseType);
}

// 404 Not Found 에러를 처리하는 함수입니다.
private static Mono<? extends Throwable> handleNotFoundError(ClientResponse clientResponse) {
    return clientResponse.createException()
            .flatMap(error -> Mono.error(new RuntimeException("Not Found")));
}

private Mono<? extends Throwable> handleErrorResponse(ClientResponse response) {
    return response.bodyToMono(String.class)
            .flatMap(errorBody -> {
                System.err.println("Error Status Code: " + response.statusCode());
                System.err.println("Error Response Body: " + errorBody);
                return Mono.error(new RuntimeException("HTTP Error: " + response.statusCode()));
            });
}

`onStatus`는 응답 상태 코드가 오류인 경우에만 실행되는로직입니다. 주로 오류 상태 코드에 따라 특정 동작을 수행하거나 에러를 처리하는 로직을 추가할 때 사용합니다. 만약 첫 번째 `onStatus()`의 조건이 true가 되어 걸리게 되면 나머지 뒤에 `onStatus()`는 무시 됩니다.  

 

💁‍♂️ `.onStatus`와 `.doOnError` 차이

둘 다 오류 처리와 관련된 메서드지만,  `.onStatus`는 응답 상태 코드를 기준으로 오류 처리를 하고, `.doOnError`는 Mono나 Flux의 오류 처리를 위해 사용됩니다. 따라서 두 메서드는 서로 다른 상황에서 사용되며, 기능도 다르게 동작합니다.

WebClient 동기적으로 사용하기

`WebClient`는 기본적으로 비동기적인 방식으로 동작합니다. `WebClient`가 동기적인 호출을 수행하기 위해선 `block()` 메서드를 사용해야합니다. `block()` 메서드를 호출하면 응답이 도착할 때까지 현재 스레드가 블록되고, 응답을 받은 후 해당 값을 반환합니다.  

import org.springframework.web.reactive.function.client.WebClient;

public class WebClientSyncExample {

    public static void main(String[] args) {
        String apiUrl = "http://example.com";

        WebClient webClient = WebClient.create(apiUrl);

        String response = webClient.get()
                .uri("/path")
                .retrieve()
                .bodyToMono(String.class)
                .block(); // 동기적으로 호출하여 응답을 받습니다.

        System.out.println("Response: " + response);
    }
}