최근 갑작스럽게, JEUS WAS 서버가 503 에러를 반환하는 일이 있었다.
503 에러란 서버가 요청을일시적으로요청을 처리할 수 없음을 나타낸다.
해당 에러를 접하고 나서, WAS 서버의 로그를 찾아 봤을 때 아래와 같은 문장들이 검출 되었다.
Request is queued because all eligible server processes are in use
Closing SPR connection because server idle timeout(300) has expired
이는 각각 다음과 같은 의미를 지니고 있다.
Request is queued because all eligible server processes are in use
→ 서버 프로세스가 모두 사용 중일 때 발생하며, 현재 요청을 처리할 수 있는 가용 프로세스가 없어서 요청이 대기열에 쌓이게 된 상태이다.
Closing SPR connection because server idle timeout(300) has expired
→ WAS에서 서버와의 연결이 일정 시간 동안 사용되지 않아 자동으로 종료되었음을 나타낸다.
이를 통해 특정 외부 API와 요청간 해당 에러가 발생했다 추측 하게 되었다.
현재 WAS에서 통신 중인 외부 API 목록을 살펴보다가, 특정 API와 연결 이후 위 에러가 검출 된 것을 확인 하였다.
해당 API에 대해서 테스트를 진행 해본 결과 현재 해당 API 서버 자체가 불안정한 상태였다.
내가 보낸 API 요청에 대한 응답을 받지 못하며, 해당 요청이 나머지 요청을 Block 하게 된 것이었다.
무슨 이유로 이러한 현상이 일어났고, 어떻게 요청들이 Block 되었는지와 어떤 방식으로 해결 하였는지에 대해 중점적으로 살펴보고자 한다.
RestTemplate은 Spring MVC에서 제공하는 외부와 HTTP 통신을 하게 도와주는 라이브러리이다.
외부 API 서버와의 통신을 통해 API를 요청할 때 일반적으로 RestTemplate를 많이 사용한다.
이 RestTemplate는 외부 API 서버에 요청을 보내는데, 이 때 위 문제를 일으킨 범인이자 가장 크리티컬한 특징이 있다.
바로 RestTemplate의 default Timeout은 제한이 없다는 것이다.
이 말 그대로, 만약 외부 API 서버가 특정 사유로 인해 응답을 주지 않는다면 무한히 기다리게 된다는 것이다.

이게 무한 츠쿠요미지
그렇다면 이러한 궁금증이 들 수도 있을 것 같다.
아니 요청을 보내고, 다른 행동을 취하거나 다른 쓰레드로 다른 동작을 하면 되잖아?
RestTemplate은 바로 동기식 클라이언트 라이브러리이며, Blocking I/O를 사용한다.

조금 더 자세하게 설명하면 아래와 같이 동작한다.
Client로부터 Request를 받을 때마다 Queue에 쌓이고, 가용 가능한 쓰레드가 있으면 해당 쓰레드에 Queue에 등록된 Request를 할당하여 처리하는 방식이다.
이 방식은 즉 하나의 Request당 하나의 쓰레드가 할당이 된다.
이 때 쓰레드가 일을 하고 있는 동안 해당 쓰레드는 Blocking 되어 다른 요청을 받을 수 없다.
그렇다면 모든 쓰레드가 일을 하고 있다면?

당연히 Queue에 Request들이 쓰레드들을 대기하게 되는 상황이 올 것이다.
RestTemplate는 동기 방식으로 운영 되기에 요청을 보내고 응답을 받을 때까지 해당 쓰레드는 무한히 기다리게 된다.
만약 서버의 쓰레드들이 모두 RestTemplate의 요청을 보내는데 사용이 되었다면, 그리고 해당 요청에 대한 응답이 무한히 오지 않는다면 서론에 이야기 한 에러의 원인이 되어진다.
그렇기에 만약 RestTemplate를 사용하게 된다면 무한 츠쿠요미로딩을 피하기 위해서 기본적으로 아래와 같이 설정하는 것이 좋다.

RestTemplate은 위에서 설명했듯이 default Timeout은 무한대이다.
그렇기에 Timeout에 대해 시간 설정을 해주면 된다.
일반적으로는 3초에서 5초 정도의 시간으로 설정을 한다.
만약 3초로 설정을 했다면, 3초의 시간이 지나도 응답이 없다면 Connection을 강제로 끊어주도록 하는 것이 이 Timeout 설정이다.
@Configuration
class RestTemplateConfig {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
.setConnectTimeout : 서버에 연결을 시도 할 때 의 연결 타임아웃 시간을 의미한다.
.setReadTimeout : 서버로부터 데이터를 읽을 때의 읽기 타임아웃 시간을 의미한다.
이 Timeout 설정은 위와 같이 설정 파일의 Bean으로 등록하여, 기본 Timeout 설정 값으로 5초를 설정 하면 된다.
이 때 Springboot를 사용하면 spring-boot-starter-web 라이브러리를 사용할 때 자동으로 여러 필수적인 빈들을 구성해준다. ← 이를 자동 구성(Auto Configuration)이라 한다.
RestTemplateBuilder도 이러한 필수적인 빈들 중 하나이며, 이는 Spring boot가 애플리케이션을 시작할 때 자동으로 Spring Container에 빈으로 등록 시키기에 별도 추가 등록 작업 없이도 위와 같이 RestTemplateBuilder를 메서드 인자로 주입 받아 사용할 수 있다.
만약 Spring MVC를 사용한다면 위 자동 구성 방식을 지원받지 못하기에 아래와 같이 SimpleClientHttpRequestFactory 객체를 사용해 작성하면 된다.
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 연결 타임아웃 (5초)
factory.setReadTimeout(5000); // 읽기 타임아웃 (5초)
return new RestTemplate(factory);
}
}

다른 방법으로는 Retry를 설정하는 방법이 있다.
Retry는 말 그대로 API 통신에 실패했을 때 일정 횟수만큼 요청을 재시도 할 수 있게 하는 기능이다.
RestTemplate에 Retry를 적용하기 위해서는 먼저 Spring-retry 프로젝트의 의존성을 추가 해야 한다.
implementation 'org.springframework.retry:spring-retry:1.2.5.RELEASE'
Spring Retry는 실행에 실패한 메서드를 자동으로 다시 시도하게 해주는 기능을 제공하며, 이를 통해 네트워크 불안정성이나 서버의 장애로 인해 요청이 실패하는 상황을 처리할 수 있다.
Spring Retry는 RetryTemplate를 통해 Retry를 편리하게 구현할 수 있도록 지원한다.
이를 RestTemplate가 실행될 때 Interceptor를 통해 RetryTemplate가 적용되게 설정하면 된다.
먼저 RestTemplate에 Interceptor를 등록해준다.
@Configuration
class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.additionalInterceptors(clientHttpRequestInterceptor())
.build();
}
}
그 후 RestTemplate을 적용할 ClientHttpRequestInterceptor를 생성 후 반환하는 메서드를 구현한다.
public ClientHttpRequestInterceptor clientHttpRequestInterceptor() {
return (request, body, execution) -> {
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2));
try {
return retryTemplate.execute(context -> execution.execute(request, body));
} catch (Throwable throwable) {
throw new RuntimeException(throwable);
}
};
}
위 메서드에서는 2회 재시도를 하게끔 설정이 되어진 RetryTemplate 객체를 생성하고, ClientHttpRequestInterceptor 객체가 실행될 때 RetryTemplate이 실행되게 한다.
출처 : https://mangkyu.tistory.com/256
이번 글에서는 RestTemplate의 단점인 Blocking I/O와 동기식 방식을 Timeout과 Retry를 통해 보완할 수 있다는 것을 다뤘다.
긴급하게 잘 처리했지만, RestTemplate의 무한 응답 대기로 인해 WAS 자체의 모든 Request가 Block 되는 Critical한 경험이었다.
이런 일들을 겪고, 해결 방법을 배울 때 마다 침착함이란 경험에서부터 나오는게 아닐까라는 생각이 들었다.
뿐만 아니라 누군가가 이번과 같은 어려움을 겪을 때 해결 방법을 제시 해줄 수 있는 그런 선배 개발자가 되고 싶다는 생각 또한 하게 되었다.