Spring WebClient, 제대로 사용하기 - exchange

2022. 8. 25. 23:04Spring

Spring WebClient의 retrieve를 사용한 요청 방법과 Exception Handling 방법을 알아보고, 테스트해보는 것이 본 포스팅의 목표입니다.

 

 

| 이어지는 포스팅  |

#1. WebClient 소개                                 : Spring WebClient, 어렵지 않게 사용하기

#2. WebClient.retrieve() 통신 방법    : Spring WebClient, 제대로 사용하기 - retrieve

#3.WebClient.exchange() 통신 방법 : 현재 포스팅

 

Reactive Programming의 전반적인 흐름 및 개념은 Reactive Programming, 제대로 이해하기를 참고해주세요.

 


 

 

안녕하세요.

이번 포스팅에서는 exchange를 사용한 WebClient 통신 방법과 Error Handling을 알아보겠습니다.

 

WebClient를 사용하다가 예외처리를 어떻게 잘할 수 있을지 고민을 많이 하게 됐는데요.

이것저것 해보면서 다듬은 실제 사용 코드를 소개하고자 합니다.

 

지난 포스팅에도 다뤘다시피, WebClient에서 응답을 받을 때에는 아래의 두 가지 메소드 중 하나를 선택해서 사용하시면 됩니다.

 

- retrieve() : body를 받아 디코딩하는 간단한 메소드

- exchange() : ClientResponse를 상태값 그리고 헤더와 함께 가져오는 메소드

 

찾아보니 exchange()를 통해 세세한 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면서 메모리 누수 가능성 때문에 retrieve()를 권고하고 있습니다.

 

받은 응답은 bodyToFlux, bodyToMono 형태로 가져와 각각 Flux와 Mono 객체로 바꿔줍니다.

Mono 객체는 0-1개의 결과를 처리하는 객체이고, Flux는 0-N개의 결과를 처리하는 객체입니다.

 

실제 사용법에 대해 다루는 이 번 포스팅에서는 retrieve()를 사용해서 요청하는 테스트를 진행해보았습니다.

지난 포스팅에서 WebClient의 설정 방식과 Custom Exception인 BadWebClientRequestException을 소개했기 때문에 이번 포스팅에서는 생략하겠습니다.

 

 

WebClient Request

테스트를 통해 exchangeToMono, exchangeToFlux의 사용법을 소개하고자 통신 메서드를 작성했습니다.

테스트 내용에 대해 정리하면 다음과 같습니다.

 

status200 : 기본적인 사용법. 요청 후 성공을 리턴받음

status4xx : 클라이언트 요청 오류. 4xx번대의 status code를 받는다면 어떻게 처리할지를 고려합니다. 

status5xx : 요청하고자하는 서버에 문제가 있을 수도 있겠죠. 5xx번대의 status code를 받을 때의 처리를 어떻게 할지 고려합니다.

 

 

📌 exchangePostForMono

private Mono<ResDTO> exchangePostForMono(String uri, MultiValueMap<String, String> body) throws WebClientResponseException {
        return webClient
            .post()
            .uri(uri)
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromValue(body))
            .exchangeToMono(response ->
                response.bodyToMono(ResDTO.class)
                    .map(validReqVO -> {
                        if (response.statusCode().is2xxSuccessful()) {
                            log.info("API 요청에 성공했습니다.");
                            return validReqVO;
                        }

                        if (response.statusCode().is4xxClientError()) {
                            log.error("API 요청 중 4xx 에러가 발생했습니다. 요청 데이터를 확인해주세요.");
                            throw new BadWebClientRequestException(response.rawStatusCode(), String.format("4xx 외부 요청 오류. statusCode: %s, response: %s, header: %s", response.rawStatusCode(), response.bodyToMono(String.class), response.headers().asHttpHeaders()));
                        }

                        log.error("API 요청 중 Tree 서버에서 5xx 에러가 발생했습니다.");
                        throw new WebClientResponseException(response.rawStatusCode(), String.format("5xx 외부 시스템 오류. %s", response.bodyToMono(String.class)), response.headers().asHttpHeaders(), null, null);
                    })
            );
}

 

위와 같은 공통 요청 메소드인 exchangePostForMono를 작성했습니다.

HTTP Method는 post를 사용했는데요. 

GET 요청이나 다른 요청 메소드를 사용하고 싶다면 post() 부분만 수정하면 됩니다.

단, get() 을 사용할 경우에는 body() 메서드를 당연히 사용할 수 없습니다.

 

이전 포스팅과 비교해보시면 아시겠지만, 클라이언트 코드(통신 메소드 호출부)는 동일하게 작성했습니다.

 

 

 

 

📌 Status Code 200 

Blocking

@Test
public void reqApiTest200() {
    // when  
    String URI = "http://127.0.0.1:8080/test/200";
    MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();

    // given: blocking
    ResDTO resDTO = this.exchangePostForMono(URI, requestBody).block();

    // then
    Assertions.assertTrue(resDTO.getStatus().is2xxSuccessful());
}

 

Non-Blocking

@Test
public void exchange_NonBlock_200() {
    String URI = HOST + "/test/200";
    MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();

    StepVerifier.create(this.exchangePostForMono(URI, requestBody))
        .expectNext(ResResult.success("Success Request"))
        .verifyComplete();
}

 

 

/test/200은 Status Code 200을 리턴하는 테스트 API입니다.

 

 

위와 같이 성공적으로 ResDTO를 받았습니다.

 

 

 

📌 Status Code 4xx

@Test
public void exchange_NonBlock_400() {
    String URI = HOST + "/test/400";
    MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();

    StepVerifier.create(this.exchangePostForMono(URI, requestBody))
        .expectErrorMatches(res ->
            res instanceof BadWebClientRequestException && res.getMessage().equals("4xx 외부 요청 오류"))
        .verify();
}

 

/test/400은 Status Code 400을 리턴하는 테스트 API입니다.

4xx 영역대의 클라이언트 요청 오류를 테스트하기 위해 제작했습니다.

 

 

위에서 제작한 통신 함수 exchangePostForMono의 코드를 보면, 

exchangeToMono 메소드의 인자로 넘겨준 핸들러 내부에서 분기처리하고 있는 아래의 코드에 걸릴 것입니다. 

 

.exchangeToMono(response -> 
    response.bodyToMono(ResDTO.class).map(validReqVO -> {
        // 200 처리 ...
        
        if (response.statusCode().is4xxClientError()) {
            log.error("API 요청 중 4xx 에러가 발생했습니다. 요청 데이터를 확인해주세요.");
            throw new BadWebClientRequestException(response.rawStatusCode(), String.format("4xx 외부 요청 오류. statusCode: %s, response: %s, header: %s", response.rawStatusCode(), response.bodyToMono(String.class), response.headers().asHttpHeaders()));
        }
        // 500 처리...
    }
}

 

실제 테스트 결과를 확인해보겠습니다.

 

 

 

테스트 결과, 위와 같은 로그를 확인할 수 있습니다.

첫 번째 줄은 위의 발췌한 코드인 exchangeToMono의 핸들러 Function에서 log.error로 출력되는 부분이고,

두 번째 줄은 try-catch 문에서 BadWebClientRequestException을 잡아서 출력한 로그입니다.

 

 

 

📌 Status Code 5xx

@Test
public void exchange_NonBlock_500() {
    String URI = HOST + "/test/500";
    MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();

    StepVerifier.create(this.exchangePostForMono(URI, requestBody))
        .expectErrorMatches(res ->
            res instanceof TargetServerErrorException &&
                res.getMessage().equals("5xx 외부 시스템 오류"))
        .verify();
}

 

/test/500은 Status Code 500을 리턴하는 테스트 API입니다.

5xx 영역 대의 대상 서버의 시스템 오류를 테스트하기 위해 제작했습니다.

 

위에서 제작한 통신 함수 exchangePostForMono의 코드를 보면, 

exchangeToMono 메소드의 인자로 넘겨준 핸들러 내부에서 분기처리하고 있는 아래의 코드에 걸릴 것입니다. 

 

.exchangeToMono(response -> 
    response.bodyToMono(ResDTO.class).map(validReqVO -> {
        // 200, 400 처리...
        log.error("API 요청 중 Tree 서버에서 5xx 에러가 발생했습니다.");
        throw new WebClientResponseException(response.rawStatusCode(), String.format("5xx 외부 시스템 오류. %s", response.bodyToMono(String.class)), response.headers().asHttpHeaders(), null, null);
    }
}

 

실제 테스트 결과를 확인해보겠습니다.

 

 

테스트 결과, 위와 같은 로그를 확인할 수 있습니다.

첫 번째 줄은 위의 발췌한 코드인 exchangeToMono의 핸들러 Function에서 log.error로 출력되는 부분이고,

두 번째 줄은 try-catch 문에서 WebClientResponseException을 잡아서 출력한 로그입니다.

 

 

📌 WebClient Error

이번엔 요청 시 StatusCode로 분기되는 예외가 아닌 WebClient 자체에서 발생하는 예외를 핸들링해보겠습니다.

 

@Test
public void exchange_NonBlock_Timeout() {
    String URI = HOST + "/test/timeout";
    MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();

    StepVerifier.create(this.exchangePostForMono(URI, requestBody))
        .expectError(WebClientRequestException.class)
        .verify();
}

 

WebClient 예외로는 타임아웃이나 가져오는 buffer 사이즈를 넘은 경우 등이 있습니다.

테스트로는 Timeout을 걸어봤습니다.

 

WebClient Configuration을 보시면 Connection Timeout을 5초로 잡아두었는데요.

/test/timeout으로 요청을 보내면 Thread.sleep으로 10초를 쉬게 됩니다.

그럼 요청 중 WebClient가 연결을 끊어내고 예외를 터뜨리겠죠.

 

 

첫 번째, 두 번째 줄은 WebClient가 출력하는 로그이고, 

두 번째 줄은 try-catch 문에서 WebClientException을 잡아서 출력한 로그입니다.

 

 

 

📌 doOnError

Mono, Flux를 처리하면서 발생하는 예외 시 실행되는 doOnError를 사용할 수도 있습니다.

doOnError 에 대한 참고 문서는 projectreactor.io 문서 에서 확인할 수 있습니다.

 

doOnError(Consumer<? super Throwable> onError)

 

doOnError

 

Mono, Flux의 처리 중 오류가 발생하면 doOnError의 onError 핸들러가 실행되는데요.

Consumer 타입으로 받으며, 주로 디버깅 용으로 사용할 수 있습니다.

 

webClient
	 // ...
	.exchangeToMono(/* ... throw any exception */)
	.doOnError(error -> {
		log.error("doOnError: " + error);
	});
    
// or

webClient
	 // ...
	.onStatus(/* ... throw any exception */)
	.doOnError(error -> {
		log.error("doOnError: " + error);
	});

 

exchangeToMono에서 Exception을 던지기 때문에 doOnError에서 해당 Exception를 받아 Consume 할 수 있는 것입니다.

 

 

 

Full Test Code

모든 코드는 해당 🔗 Github 링크에서 확인할 수 있습니다.

 

 

그럼 지금까지 WebClient의 exchange 사용법과 예외처리에 대해 알아보았습니다.

오타나 잘못된 내용을 댓글로 남겨주세요!

감사합니다 ☺️