메모 목적으로 작성한 글이라서 생략된 부분이 많습니다.

 

  1.  목적/배경
    1. 현재 JDK 21의 VirtualThread기반으로 작업 중
    2. webflux를 이용해서 논블럭을 작성할 필요가 없어짐
      1. 유지보수, 읽기좋은 소스 등의 관점에서 기존 동기방식 스타일 코드 작성이 유리
      2. IO 블럭킹에 대한 성능 문제는 VirtualThread가 blocking 코드를 만나면 잠시 대기/큐잉 등 의 형태로 커버됨
    3. 다만, 통신용 모듈이 webclient로 기존에 작성되어 있음
      1. spring 디펜더시 문제 등의 이유로 23년 11월 23일에 spring boot 3.2가 릴리즈될때 rest client가 포함되서 해결 예정
      2. 지금 당장 webclient로 작성된 코드도 필요
    4. webclient를 block()으로 호출해서 임시 사용

 

------

샘플 소스

/**
 * Webclient로 외부 API를 호출
 *  - block()을 사용하여 결과를 받아옴
 *  - 500 에러가 발생하면 재시도
 *
 * @author 
 */
@Slf4j
public class WebClientBlockRetriveRequestSample {

  public static void main(String[] args) {

    final String reqUri = "http://localhost:87/delay/2"; //테스트 대상 URL
    final Duration timeoutDuration = Duration.ofSeconds(1); //Timeout

    String apiResponse = null;
    try {
      apiResponse = getRequestExcute(reqUri, timeoutDuration); //요청 실행
    } catch (BadWebClientRequestException e) {
      log.error("BadWebClientRequestException발생\n\n\t{}", e.getMessage(), e);
      throw e;
    } catch (WebClientTimeoutException te) {
      log.error("WebClientTimeoutException발생\n\n\t{}", te.getMessage(), te);
    }

    log.info("apiResponse: {}", apiResponse);

  }

  /**
   * 외부 HTTP 요청 실행
   *
   * @param reqUri
   * @param timeoutDuration
   * @return
   */
  public static String getRequestExcute(String reqUri, Duration timeoutDuration) {

    WebClient webClient = WebClient.builder()
        //.defaultHeader("Content-Type", "application/json")
        .build();

    String apiResponse = webClient.mutate().build().get()
        .uri(reqUri)
        .retrieve()
        .onStatus(httpStatus -> httpStatus.is4xxClientError() || httpStatus.is5xxServerError(),
            clientResponse -> handleErrorResponse(reqUri, clientResponse)
        ).bodyToMono(String.class)
        .timeout(timeoutDuration)
        .doOnError(throwable -> {

          if (throwable instanceof java.util.concurrent.TimeoutException) { //타임아웃 발생한 경우 핸들링을 위해서 예외 클래스 변경 처리
            log.error("TimeoutException: " + throwable.getMessage());
            throw new WebClientTimeoutException(String.format("Steam API no response whthin %s(millis)", timeoutDuration.toMillis()));
          }

        })
        .retryWhen(Retry.backoff(2, Duration.ofSeconds(2)).maxBackoff(Duration.ofSeconds(3)).jitter(0.5)
            .filter(throwable -> throwable instanceof WebClientNeedRetryException)) //특정 예외인 경우 재 시도
        .block(); //동기 방식으로 호출(virtual thread사용하기 때문에 문제 없음)

    return apiResponse;
  }


  public static Mono<? extends Throwable> handleErrorResponse(String uri, ClientResponse response) {

    if (response.statusCode().is4xxClientError()) {
      String errMsg = String.format("'%s' 4xx ERROR. statusCode: %s, response: %s, header: %s", uri, response.statusCode().value(), response.bodyToMono(String.class), response.headers().asHttpHeaders());
      log.error(errMsg);
      return Mono.error(new BadWebClientRequestException(response.statusCode().value(), errMsg));
    }

    if (response.statusCode().is5xxServerError()) { //5xx에러인 경우 재 시도 처리를 위해서 재 시도 필요 예외를 리턴
      String errMsg = String.format("'%s' 5xx ERROR. %s", uri, response.toString());
      log.error(errMsg);
      return Mono.error(new WebClientNeedRetryException(response.statusCode().value(), errMsg));
    }

    String errMsg = String.format("'%s' ERROR. statusCode: %s, response: %s, header: %s", uri, response.statusCode().value(), response.bodyToMono(String.class), response.headers().asHttpHeaders());
    log.error(errMsg);
    return Mono.error(new RuntimeException(errMsg));

  }

 

사용하는 커스텀 개발된 예외들

/**
 * 잘못된 파라미터로 요청시 발생하는 Exception
 *
 * @author 
 */
@Getter
public class BadWebClientRequestException extends RuntimeException {

  private static final long serialVersionUID = 2241080498857315158L;

  private final int statusCode;

  private String statusText;

  public BadWebClientRequestException(int statusCode) {
    super();
    this.statusCode = statusCode;
  }

  public BadWebClientRequestException(int statusCode, String msg) {
    super(msg);
    this.statusCode = statusCode;
  }

  public BadWebClientRequestException(int statusCode, String msg, String statusText) {
    super(msg);
    this.statusCode = statusCode;
    this.statusText = statusText;
  }
}

 

/**
 * Webclient로 호출 중 재 시도가 필요한 경우에 사용하는 Exception
 *
 * @author 
 */
@Getter
public class WebClientNeedRetryException extends RuntimeException {

  private static final long serialVersionUID = 3238789645114297396L;

  private final int statusCode;

  private String statusText;

  public WebClientNeedRetryException(int statusCode) {
    super();
    this.statusCode = statusCode;
  }

  public WebClientNeedRetryException(int statusCode, String msg) {
    super(msg);
    this.statusCode = statusCode;
  }

  public WebClientNeedRetryException(int statusCode, String msg, String statusText) {
    super(msg);
    this.statusCode = statusCode;
    this.statusText = statusText;
  }

+ Recent posts