본문 바로가기
Spring

WebClient 사용할때 주의 (2편)

by ybs 2021. 6. 10.
반응형

exchangeToMono 를 쓸 때 

cf) WebClient 사용할때 주의 (1편) 에서 exchange 를 쓸 때 주의할 점을 하나 소개했는데, 이제 exchange는 deprecated 되었고 대신 exchangeToMono 나 exchangeToFlux 를 써야한다.

 

1) doOnSuccess 와 onErrorResume

.exchangeToMono(clientResponse -> clientResponse.toEntity(String.class))
.doOnSuccess(clientResponse -> {
		if (clientResponse.getStatusCode() != HttpStatus.OK) {
			throw new RuntimeException("error");
		}
	}
)
.onErrorResume(error -> {
	return Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
});

 

응답 해주는 서버가 비지니스 정책 상 에러로 400이나 500 에러를 리턴한다면 doOnSuccess 에 오게 되고, 상태코드가 HttpStatus.OK(200) 이 아니니 RuntimeException 예외를 던지게 되고, onErrorResume 에서 받아서 500 을 리턴하게 된다.

 

하지만 reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response 에러나 Connection reset by peer 에러를 강제로 발생시켰을 때는 doOnSuccess 를 거치지 않고 바로 onErrorResume 으로 가서 500을 리턴하게 된다.

 

doOnSuccess 이름만 들었을 때는 요청 응답이 HttpStatus.OK 만 받을거 같지만 400이든 500이든 응답이 제대로 온다면 doOnSuccess 가 받는다.

 

그리고 처음할 때 흔히 하는 실수로 doOnSuccess 가 Consumer 타입을 받기 때문에 리턴문이 없으니 아래처럼 Mono.error 를 작성할 수 있다. 하지만 Mono.error() 도 Mono 이기 때문에 subscribe 하지 않으면 실행되지 않는다.

// 주의
.doOnSuccess(clientResponse -> {
		if (clientResponse.getStatusCode() != HttpStatus.OK) {
			Mono.error(new RuntimeException("no!!")); 
		}
	}
);

 

※주의

doOnSuccess 에 대해 "400이든 500이든 응답이 제대로 온다면 doOnSuccess 가 받는다" 라고 했는데 이건 exchangeToMono 나 exchangeToFlux 같은 exchange 로 받았을 때다. 만약 아래와 같이 retrieve 를 사용해 응답을 받는다면 4xx, 5xx 응답은 doOnSuccess로 가지 않는다.

.retrieve()
.bodyToMono(String.class)
.doOnSuccess(
	responseBody -> {
		System.out.println("responseBody : " + responseBody);
	}
)

 

 

2) exchangeToMono 안에서 처리하고 onErrorResume

.exchangeToMono(clientResponse -> {
	if (clientResponse.rawStatusCode() == HttpStatus.BAD_REQUEST.value()) {
		return clientResponse
			.createException()
			.flatMap(it -> Mono.error(new RuntimeException("400!!")));
	}

	if (clientResponse.rawStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
		return clientResponse
			.createException()
			.flatMap(it -> Mono.error(new RuntimeException("500!!")));
	}

	return clientResponse.bodyToMono(String.class);
})
.onErrorResume(error -> {
	return Mono.error(new RuntimeException(error.getMessage()));
});

응답 해주는 서버가 비지니스 정책 상 에러로 400이나 500 에러를 리턴한다면 위의 if 문을 타게 되고, RuntimeException 을 감싼 Mono.error 를 리턴한다. 그리고 onErrorResume 에서 다시 받아서 Mono.error 를 다시 리턴한다.

 

만약 이렇게 작성을 한다면 아래와 같이 RuntimeException 을 받아주는 핸들러를 따로 만들거나

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public String runtimeExceptionHandler(RuntimeException ex) {
	return "runtimeException";
}

저 webClient 응답을 받는 쪽에서 적절한 추가 예외처리가 필요하다.

 

여기서 reactor.netty.http.client.PrematureCloseException: Connection prematurely closed DURING response 에러를 강제로 발생시키면 exchangeToMono 람다 바디안에서 동작하던중 onErrorResume 으로 가게 된다. 그리고 Connection reset by peer 에러를 강제로 발생시켰을 때는 onErrorResume 으로 바로 간다.

 

PrematureCloseException 에러를 재현을 응답 서버에서 response header만 보내고 response body 안보내는 방식으로 만들었다(응답 도중에 끊기게 되는 상황). 그러다보니 헤더는 정상적으로 전달되서 exchangeToMono 람다 바디안까지는 정상적으로 동작하게 된다.

 

그런데 위에서 400/500 status code로 정상적으로 응답 올 때, throw Exception 하거나 flatMap 으로 Mono.error 로 감싸서 리턴했기 때문에 onErrorResume 으로 간거다. 400/500 이라고 반드시 onErrorResume 으로 간다고 생각하면 안된다.

 

아래 코드처럼 ResponseEntity 에 response header, response body 가 담아진 상태에서 리턴도록 만들면  400/500 이 정상적으로 응답 올 때 onErrorResume 으로 안간다. 

.exchangeToMono(clientResponse -> {
	return clientResponse.toEntity(String.class);
})
.onErrorResume(throwable -> {
	return Mono.just(new ResponseEntity<>("HAHAHA", HttpStatus.INTERNAL_SERVER_ERROR));
});

 

cf) Connection reset 에 대한 자세한 내용은 https://yangbongsoo.gitbook.io/study/connection_reset 여기에 따로 정리되어 있다. Connection reset 발생 원인은 다양하기 때문에 원인을 한가지로 볼 수는 없다. 여기서 재현한 방식은 응답서버의 커넥션을 강제로 close 시켜서 통신을 끊기게 만들었다.

 

3) retrieve 와 onErrorResume

스프링 메인테이너는 retrieve 를 사용하도록 권장한다. exchange 방식을 쓰면 응답에 대해서 직접 처리를 해야되기 때문에 메모리 릭이나 커넥션 릭 발생할 수 있기 때문이다(https://github.com/reactor/reactor-netty/issues/1401#issuecomment-736393872).

 

그래서 retrieve 방식으로 바꾸면 아래와 같이 작성할 수 있는데, 응답 상황별 흐름은 다르지 않다.

.retrieve()
.onStatus(
  httpStatus -> httpStatus != HttpStatus.OK,
    clientResponse -> {
      return clientResponse.createException()
         .flatMap(it -> Mono.error(new RuntimeException("code : " + clientResponse.statusCode())));
      })
  .bodyToMono(String.class)
  .onErrorResume(throwable -> {
    return Mono.error(new RuntimeException(throwable));
});
반응형