Setting Connection Timeout and Read Timeout for Jersey

1. 소개

이 튜토리얼에서는 REST 클라이언트에서 연결 및 읽기 타임아웃을 구성하는 것의 중요성을 살펴보겠습니다. 일반적인 JAX-RS 구현체인 Jersey를 사용하여 이 내용을 시연할 것입니다.

2. 왜 연결 및 읽기 타임아웃을 설정해야 할까요?

소켓에 대한 타임아웃 설정은 응답성과 신뢰성이 중요한 애플리케이션에서 필수적입니다. 예를 들어, 지연은 사용자 경험에 영향을 미치거나 금융 또는 전자상거래 애플리케이션에서 거래 실패를 초래할 수 있습니다.

마찬가지로, 분산 시스템에서 잘못 구성된 타임아웃은 연쇄적인 지연과 자원 병목 현상을 초래할 수 있습니다. 적절한 타임아웃 값을 선택하면 네트워크 조건이 어려울 때에도 애플리케이션이 강력하고 반응성을 유지할 수 있습니다.

3. 종속성과 기본 설정

타임아웃이 어떻게 작동하는지 이해하기 위해, 느린 REST API를 호출하도록 Jersey 클라이언트를 구성하고 응답이 설정된 타임아웃보다 더 오래 걸릴 때의 동작을 관찰할 것입니다.

3.1. 종속성

클라이언트 측에서는 API와의 HTTP 통신을 위해 jersey-client가 필요합니다:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>3.1.9</version>
</dependency>

서버 측에서는 jersey-server가 필요합니다:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-server</artifactId>
    <version>3.1.9</version>
</dependency>

테스트에서 서버를 실행하기 위해 jaxrs-ri, jersey-container-grizzly2-servlet, 그리고 jersey-test-framework-provider-grizzly2가 필요합니다:

<dependency>
    <groupId>org.glassfish.jersey.bundles</groupId>
    <artifactId>jaxrs-ri</artifactId>
    <version>3.1.9</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-grizzly2-servlet</artifactId>
    <version>3.1.9</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.test-framework.providers</groupId>
    <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
    <version>3.1.9</version>
    <scope>test</scope>
</dependency>

3.2. REST API 서버

REST API에는 테스트에 사용할 단일 엔드포인트가 포함되어 있습니다. 작업의 구현은 중요하지 않으며, 지연된 응답을 시뮬레이션하는 것이 목표입니다. 느린 서버를 시뮬레이션하기 위해 약간의 대기 시간을 도입할 것입니다:

@Path("/timeout")
public class TimeoutResource {

    public static final long STALL = TimeUnit.SECONDS.toMillis(2L);

    @GET
    public String get() throws InterruptedException {
        Thread.sleep(STALL);
        return "processed";
    }
}

이후에 STALL 변수를 사용하여 타임아웃 값을 결정할 것입니다.

3.3. 클라이언트 설정

클라이언트는 엔드포인트 URI와 이 엔드포인트에 접근하기 위한 일반적인 get(Client) 메소드만 필요합니다. 또한 이전에 정의한 STALL 변수의 절반으로 우리 TIMEOUT을 정의하여 타임아웃 시나리오를 설정합시다:

public class JerseyTimeoutClient {

    private static final long TIMEOUT = TimeoutResource.STALL / 2;

    private final String endpoint;

    public JerseyTimeoutClient(String endpoint) {
        this.endpoint = endpoint;
    }

    private String get(Client client) {
        return client.target(endpoint)
          .request()
          .get(String.class);
    }

    // ...
}

3.4. 테스트 설정

우리는 읽기 타임아웃을 테스트하는 두 개의 클라이언트를 정의할 것입니다. 하나는 읽기 타임아웃을 테스트하고, 다른 하나는 연결 타임아웃을 테스트할 것입니다.

우선 엔드포인트의 기본 주소부터 설정합니다:

static final URI BASE = URI.create("http://localhost:8082");

읽기 타임아웃을 테스트하기 위해 올바른 엔드포인트를 사용합니다. 서버가 요청을 처리하는 데 걸리는 시간의 절반이 타임아웃이기 때문에 SocketTimeoutException이 보장됩니다:

static final String CORRECT_ENDPOINT = BASE + "/timeout";
JerseyTimeoutClient readTimeoutClient = new JerseyTimeoutClient(CORRECT_ENDPOINT);

그런 다음, 연결 타임아웃을 테스트하기 위해, 도달할 수 없는 IP 주소로 호스트를 교체합니다:

static final String INCORRECT_ENDPOINT = BASE.toString()
  .replace(BASE.getHost(), "10.255.255.1"); 

JerseyTimeoutClient connectTimeoutClient = new JerseyTimeoutClient(INCORRECT_ENDPOINT);

마지막으로, 우리는 테스트에서 재사용할 수 있는 어설션 메소드를 추가합니다. 이 메소드는 JAX-RS에서 발생하는 ProcessingException을 잡는 것으로 시작하며, 원인을 확인하여 항상 SocketTimeoutException이어야 합니다. 마지막으로, 메시지를 확인하여 연결 타임아웃과 읽기 타임아웃을 구분합니다:

private void assertTimeout(String message, Executable executable) {
    ProcessingException exception = assertThrows(ProcessingException.class, executable);

    Throwable cause = exception.getCause();
    assertInstanceOf(SocketTimeoutException.class, cause);

    assertEquals(message, cause.getMessage());
}

이제 테스트할 준비가 되었으니, 타임아웃 구성 방법을 살펴보겠습니다.

4. ClientBuilder API 사용하기

JerseyTimeoutClient로 돌아가서 ClientBuilder를 사용한 첫 번째 구현을 추가해 보겠습니다. 우리는 동일한 클라이언트에 대해 두 가지 타임아웃 구성(connectTimeout()readTimeout())을 구성을 설정할 수 있습니다. 앞서 생성한 클라이언트를 get() 메소드에 전달하여 엔드포인트를 호출합니다:

public String viaClientBuilder() {
    ClientBuilder builder = ClientBuilder.newBuilder()
      .connectTimeout(TIMEOUT, TimeUnit.MILLISECONDS)
      .readTimeout(TIMEOUT, TimeUnit.MILLISECONDS);

    return get(builder.build());
}

이제 읽기 타임아웃 테스트:

@Test
void givenCorrectEndpoint_whenClientBuilderAndSlowServer_thenReadTimeout() {
    assertTimeout("Read timed out", readTimeoutClient::viaClientBuilder);
}

연결 타임아웃 테스트:

@Test 
void givenIncorrectEndpoint_whenClientBuilder_thenConnectTimeout() { 
    assertTimeout("Connect timed out", connectTimeoutClient::viaClientBuilder); 
}

이 빌더로 생성된 모든 클라이언트는 원하는 값으로 두 타임아웃을 설정합니다. 설정하지 않으면, 기본 동작은 응답을 무한정 기다리는 것이며, 이는 기본적으로 0으로 설정하는 것과 같습니다.

다음으로, 다양한 요구 사항에 따라 클라이언트에서 이러한 타임아웃을 설정하는 세 가지 대체 방법을 살펴보겠습니다.

5. ClientConfig 객체 사용하기

타임아웃은 ClientConfig를 통해 구성할 수도 있으며, 새로운 클라이언트를 빌드할 때 사용할 수 있습니다:

public String viaClientConfig() {
    ClientConfig config = new ClientConfig();
    config.property(ClientProperties.CONNECT_TIMEOUT, TIMEOUT);
    config.property(ClientProperties.READ_TIMEOUT, TIMEOUT);

    return get(ClientBuilder.newClient(config));
}

ClientConfig를 사용하면 많은 클라이언트에 동일한 구성을 적용할 수 있습니다. 이는 많은 복잡하고 동적인 구성을 재사용 가능하게 할 때 특히 유용합니다.

6. client.property() 메소드 사용하기

이 속성을 Client에서 직접 설정할 수도 있습니다:

public String viaClientProperty() {
    Client client = ClientBuilder.newClient();
    client.property(ClientProperties.CONNECT_TIMEOUT, TIMEOUT);
    client.property(ClientProperties.READ_TIMEOUT, TIMEOUT);

    return get(client);
}

이는 ClientBuilderconnectTimeout()가 제공되지 않았던 2.1 이전의 Jersey 버전에서는 필수적입니다.

7. 요청별 타임아웃 설정하기

마지막으로, 요청에서 타임아웃을 설정하는 메소드를 포함시킵니다. 요청 타임아웃 매개변수를 포함하도록 get() 메소드를 오버라이드하거나 수정합니다:

private String get(Client client, Long requestTimeout) {
    Builder request = client.target(endpoint).request();

    if (requestTimeout != null) {
        request.property(ClientProperties.CONNECT_TIMEOUT, requestTimeout);
        request.property(ClientProperties.READ_TIMEOUT, requestTimeout);
    }

    return request.get(String.class);
}

이제 원하는 요청 타임아웃 값을 전달할 수 있습니다:

public String viaRequestProperty() {
    return get(ClientBuilder.newClient(), TIMEOUT);
}

이 설정을 통해 특정 요청에 대해 전역 설정을 재정의하거나 타임아웃 값에 대한 가변 요구 사항을 가질 수 있습니다.

8. 결론

이 기사에서는 연결 및 읽기 타임아웃을 구성하는 것이 강력하고 응답성이 뛰어난 REST 클라이언트를 구축하는 데 얼마나 중요한지 배웠습니다. ClientBuilder, ClientConfig, 요청별 구성 등 Jersey에서 제공하는 다양한 접근 방식을 이해함으로써 특정 애플리케이션 요구 사항을 충족시키기 위해 클라이언트 동작을 조정할 수 있습니다.

항상 그렇듯, 소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다