1. 개요

이 튜토리얼에서는 Spring REST API에 대한 요청 제한 시간을 구현하는 몇 가지 가능한 방법을 탐색합니다.

각각의 장점과 단점에 대해 논의 할 것입니다. 요청 시간 제한은 특히 리소스가 너무 오래 걸릴 때 기본값으로 설정할 수있는 대안이있는 경우 열악한 사용자 경험을 방지하는 데 유용합니다. 이 디자인 패턴을 회로 차단기 패턴 이라고 하지만 여기서 자세히 설명하지는 않겠습니다.

2. @Transactional 타임 아웃

데이터베이스 호출에 대한 요청 시간 제한을 구현할 수있는 한 가지 방법은 Spring의 @Transactional 어노테이션 을 활용하는 것입니다 . 우리가 설정할 수 있는 타임 아웃 속성이 있습니다. 이 속성의 기본값은 -1이며, 이는 시간 제한이 전혀없는 것과 같습니다. 제한 시간 값의 외부 구성의 경우 다른 특성 인 timeoutString을 대신 사용해야합니다.

예를 들어,이 타임 아웃을 30으로 설정했다고 가정 해 봅시다. 어노테이션이있는 메소드의 실행 시간이이 시간 (초)을 초과하면 예외가 발생합니다. 이는 장기 실행 데이터베이스 쿼리를 롤백하는 데 유용 할 수 있습니다.

이것이 실제로 작동하는지보기 위해 완료하는 데 너무 오래 걸리고 시간 초과가 발생하는 외부 서비스를 나타내는 매우 간단한 JPA 저장소 계층을 작성해 보겠습니다. 이 JpaRepository 확장에는 시간이 많이 걸리는 메서드가 있습니다.

public interface BookRepository extends JpaRepository<Book, String> {

    default int wasteTime() {
        int i = Integer.MIN_VALUE;
        while(i < Integer.MAX_VALUE) {
            i++;
        }
        return i;
    }
}

타임 아웃이 1 초인 트랜잭션 내부에서 wasteTime () 메서드를 호출 하면 메서드 실행이 완료되기 전에 타임 아웃이 경과됩니다.

@GetMapping("/author/transactional")
@Transactional(timeout = 1)
public String getWithTransactionTimeout(@RequestParam String title) {
    bookRepository.wasteTime();
    return bookRepository.findById(title)
      .map(Book::getAuthor)
      .orElse("No book found for this title.");
}

이 끝점을 호출하면 500 HTTP 오류가 발생하여보다 의미있는 응답으로 변환 할 수 있습니다. 또한 구현할 설정이 거의 필요하지 않습니다.

그러나이 시간 제한 솔루션에는 몇 가지 단점이 있습니다.

첫째, Spring 관리 트랜잭션이있는 데이터베이스에 의존합니다. 또한 어노테이션이 필요한 각 메서드 또는 클래스에 있어야하므로 프로젝트에 전역 적으로 적용 할 수 없습니다. 또한 1 초 미만의 정밀도를 허용하지 않습니다. 마지막으로, 시간 제한에 도달해도 요청을 짧게 자르지 않으므로 요청하는 엔티티는 여전히 전체 시간을 기다려야합니다.

다른 옵션을 고려해 봅시다.

3. Resilience4j TimeLimiter

Resilience4j는 주로 원격 통신을위한 내결함성을 관리하는 전용 라이브러리 입니다. TimeLimiter의 모듈은 우리가 여기에 관심이있는 것입니다.

먼저 프로젝트에 resilience4j-timelimiter 의존성포함해야합니다 .

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-timelimiter</artifactId>
    <version>1.6.1</version>
</dependency>

다음 으로 제한 시간이 500 밀리 초인 간단한 TimeLimiter정의 해 보겠습니다 .

private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
  .timeoutDuration(Duration.ofMillis(500)).build());

이것은 외부에서 쉽게 구성 할 수 있습니다.

TimeLimiter사용 하여 @Transactional 예제에서 사용한 것과 동일한 논리를 래핑 할 수 있습니다 .

@GetMapping("/author/resilience4j")
public Callable<String> getWithResilience4jTimeLimiter(@RequestParam String title) {
    return TimeLimiter.decorateFutureSupplier(ourTimeLimiter, () ->
      CompletableFuture.supplyAsync(() -> {
        bookRepository.wasteTime();
        return bookRepository.findById(title)
          .map(Book::getAuthor)
          .orElse("No book found for this title.");
    }));
}

TimeLimiter는 오버 여러 가지 혜택 제공 @Transactional 솔루션을. 즉, 1 초 미만의 정밀도와 시간 초과 응답에 대한 즉각적인 알림을 지원합니다. 그러나 시간 초과가 필요한 모든 엔드 포인트에 수동으로 포함되어야하며 약간의 긴 래핑 코드가 필요하며 생성되는 오류는 여전히 일반 500 HTTP 오류입니다. 또한 원시 문자열 대신 Callable <String>을 반환해야합니다 .

TimeLimiter이 일부만 포함 Resilience4j에서 기능 좋게 차단기 패턴 및 인터페이스.

4. Spring MVC 요청 타임 아웃

Spring은 spring.mvc.async.request-timeout 이라는 속성을 제공합니다 . 이 속성을 사용하면 밀리 초 정밀도로 요청 시간 제한을 정의 할 수 있습니다.

750 밀리 초 제한 시간으로 속성을 정의 해 보겠습니다.

spring.mvc.async.request-timeout=750

이 속성은 전역 적이며 외부 적으로 구성 가능하지만 TimeLimiter 솔루션 과 마찬가지로 Callable 을 반환하는 끝점에만 적용됩니다 . TimeLimiter 예제 와 비슷 하지만 Futures 에서 로직을 래핑 하거나 TimeLimiter를 제공 할 필요가없는 엔드 포인트를 정의 해 보겠습니다 .

@GetMapping("/author/mvc-request-timeout")
public Callable<String> getWithMvcRequestTimeout(@RequestParam String title) {
    return () -> {
        bookRepository.wasteTime();
        return bookRepository.findById(title)
          .map(Book::getAuthor)
          .orElse("No book found for this title.");
    };
}

코드가 덜 장황하고 애플리케이션 속성을 정의 할 때 Spring에 의해 구성이 자동으로 구현됨을 알 수 있습니다. 제한 시간에 도달하면 응답이 즉시 반환되며 일반 500 대신 더 설명적인 503 HTTP 오류도 반환합니다. 또한 프로젝트의 모든 엔드 포인트는이 시간 제한 구성을 자동으로 상속합니다.

좀 더 세분화 된 시간 제한을 정의 할 수있는 또 다른 옵션을 고려해 보겠습니다.

5. WebClient 시간 초과

전체 끝점에 대한 시간 제한을 설정하는 대신 단일 외부 호출에 대한 시간 제한을 원할 수 있습니다. WebClient 는 Spring의 반응 형 웹 클라이언트 이며 응답 시간 제한을 구성 할 수 있습니다.

Spring의 이전 RestTemplate 객체 에 타임 아웃을 구성하는 것도 가능 합니다 . 그러나 대부분의 개발자는 이제 RestTemplate 보다 WebClient선호합니다 .

WebClient를 사용하려면 먼저 Spring의 WebFlux 의존성 을 프로젝트에 추가해야합니다 .

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <version>2.4.2</version>
</dependency>

기본 URL에서 localhost를 통해 자신을 호출하는 데 사용할 수있는 응답 시간 제한이 250 밀리 초인 WebClient정의 해 보겠습니다 .

@Bean
public WebClient webClient() {
    return WebClient.builder()
      .baseUrl("http://localhost:8080")
      .clientConnector(new ReactorClientHttpConnector(
        HttpClient.create().responseTimeout(Duration.ofMillis(250))
      ))
      .build();
}

분명히이 시간 제한 값을 외부에서 쉽게 구성 할 수 있습니다. 기본 URL은 물론 다른 여러 선택적 속성을 외부 적으로 구성 할 수도 있습니다.

이제 WebClient 를 컨트롤러에 삽입하고 이를 사용하여 1 초의 타임 아웃이있는 자체 / transactional 엔드 포인트 를 호출 할 수 있습니다 . WebClient 를 250 밀리 초 안에 시간 초과 하도록 구성 했으므로 1 초보다 훨씬 빠르게 실패하는 것을 볼 수 있습니다.

다음은 새로운 엔드 포인트입니다.

@GetMapping("/author/webclient")
public String getWithWebClient(@RequestParam String title) {
    return webClient.get()
      .uri(uriBuilder -> uriBuilder
        .path("/author/transactional")
        .queryParam("title", title)
        .build())
      .retrieve()
      .bodyToMono(String.class)
      .block();
}

이 끝점을 호출 한 후 500 HTTP 오류 응답의 형태로 WebClient 의 시간 초과를 수신하는 것을 확인합니다 . 또한 로그를 확인하여 다운 스트림 @Transactional 시간 초과 를 확인할 수 있습니다 . 그러나 물론 localhost 대신 외부 서비스를 호출하면 시간 초과가 원격으로 인쇄되었을 것입니다.

다른 백엔드 서비스에 대해 다른 요청 시간 제한을 구성해야 할 수 있으며이 솔루션으로 가능합니다. 또한 WebClient에서 반환 된 Mono 또는 Flux 응답 게시자 에는 일반 시간 초과 오류 응답을 처리하기 위한 많은 오류 처리 메서드가 포함되어 있습니다 .

6. 결론

이 기사에서 요청 시간 제한을 구현하기위한 몇 가지 다른 솔루션을 살펴 보았습니다. 사용할 요소를 결정할 때 고려해야 할 몇 가지 요소가 있습니다.

데이터베이스 요청에 타임 아웃을 적용하려면 Spring의 @Transactional 메서드와 타임 아웃 속성을 사용하는 것이 좋습니다. 더 넓은 회로 차단기 패턴과 통합하려는 경우 Resilience4j의 TimeLimiter 를 사용하는 것이 합리적입니다. Spring MVC request-timeout 속성을 사용하는 것은 모든 요청에 ​​대해 전역 제한 시간을 설정하는 데 가장 적합하지만 WebClient를 사용 하여 리소스 당보다 세부적인 제한 시간을 쉽게 정의 할 수 있습니다 .

이러한 모든 솔루션의 작동 예제의 경우 코드가 준비되어 있으며 GitHub 에서 즉시 실행할 수 있습니다 .