메모 목적으로 작성한 글이라서 생략된 부분이 많습니다.
- 목적/배경
- 현재 JDK 21의 VirtualThread기반으로 작업 중
- webflux를 이용해서 논블럭을 작성할 필요가 없어짐
- 유지보수, 읽기좋은 소스 등의 관점에서 기존 동기방식 스타일 코드 작성이 유리
- IO 블럭킹에 대한 성능 문제는 VirtualThread가 blocking 코드를 만나면 잠시 대기/큐잉 등 의 형태로 커버됨
- 다만, 통신용 모듈이 webclient로 기존에 작성되어 있음
- spring 디펜더시 문제 등의 이유로 23년 11월 23일에 spring boot 3.2가 릴리즈될때 rest client가 포함되서 해결 예정
- 지금 당장 webclient로 작성된 코드도 필요
- 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;
}
'JAVA > Spring 일반' 카테고리의 다른 글
Using Spring’s @Retryable Annotation for Automatic Retries (0) | 2024.01.10 |
---|---|
spring 캘린더(기능 업데이트 일정 등) (0) | 2023.11.17 |
JPA N+1 문제 해결과 관련 Spring Data JDBC에서 기능 추가 예정과 관련 (0) | 2023.09.01 |
spring custom valid 어노테이션으로 업로드 파일의 확장자 및 유효성 검사 (0) | 2023.07.31 |
Spring에서 ModelAndViewDefiningException 및 상황에 따른 view 또는 json 응답 (0) | 2022.05.03 |