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);
}
이는 ClientBuilder의 connectTimeout()가 제공되지 않았던 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에서 확인할 수 있습니다.