

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# 클라이언트 모범 사례(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.redis"></a>

일반적인 시나리오의 모범 사례를 알아보고 가장 인기 있는 오픈 소스 Valkey 및 Redis OSS 클라이언트 라이브러리(redis-py, PHPRedis 및 Lettuce)의 코드 예제와 함께 일반적으로 사용되는 오픈 소스 Memcached 클라이언트 라이브러리를 사용하여 ElastiCache 리소스와 상호 작용하기 위한 모범 사례를 따르세요.

**Topics**
+ [많은 수의 연결(Valkey 및 Redis OSS)](BestPractices.Clients.Redis.Connections.md)
+ [클러스터 클라이언트 검색 및 지수 백오프(Valkey 및 Redis OSS)](BestPractices.Clients.Redis.Discovery.md)
+ [클라이언트 측 제한 시간 구성(Valkey 및 Redis OSS)](BestPractices.Clients.Redis.ClientTimeout.md)
+ [서버 측 유휴 제한 시간 구성(Valkey 및 Redis OSS)](BestPractices.Clients.Redis.ServerTimeout.md)
+ [Lua 스크립트](BestPractices.Clients.Redis.LuaScripts.md)
+ [대규모 복합 항목 저장(Valkey 및 Redis OSS)](BestPractices.Clients.Redis.LargeItems.md)
+ [Lettuce 클라이언트 구성(Valkey 및 Redis OSS)](BestPractices.Clients-lettuce.md)
+ [듀얼 스택 클러스터에 선호되는 프로토콜 구성(Valkey 및 Redis OSS)](#network-type-configuring-dual-stack-redis)

# 많은 수의 연결(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.Redis.Connections"></a>

서버리스 캐시와 각 ElastiCache for Redis OSS 노드는 최대 65,000개의 동시 클라이언트 연결을 지원합니다. 하지만 성능을 최적화하려면 클라이언트 애플리케이션이 해당 연결 수준에서 계속 작동하지 않는 것이 좋습니다. Valkey와 Redis OSS는 각각 들어오는 클라이언트 요청이 순차적으로 처리되는 이벤트 루프를 기반으로 하는 단일 스레드 프로세스입니다. 즉, 연결된 클라이언트 수가 늘어날수록 해당 클라이언트의 응답 시간이 길어집니다.

Valkey 또는 Redis OSS 서버에서 연결 병목 현상이 발생하지 않도록 다음과 같이 조치를 취할 수 있습니다.
+ 읽기 전용 복제본에서 읽기 작업을 수행합니다. 이렇게 하려면 클러스터 모드가 비활성화된 상태에서 ElastiCache 리더 엔드포인트를 사용하거나, 클러스터 모드가 활성화된 상태에서 읽기 전용 복제본(서버리스 캐시 포함)을 사용하여 수행할 수 있습니다.
+ 쓰기 트래픽을 여러 프라이머리 노드에 분산합니다. 2가지 방법으로 수행할 수 있습니다. Valkey 또는 Redis OSS 클러스터 모드 지원 클라이언트와 함께 다중으로 샤딩된 클러스터를 사용할 수 있습니다. 클라이언트 측 샤딩이 비활성화된 클러스터 모드에서 여러 프라이머리 노드에 쓸 수도 있습니다. 이 작업은 서버리스 캐시에서 자동으로 수행됩니다.
+ 클라이언트 라이브러리에서 사용 가능한 경우 연결 풀을 사용하세요.

일반적으로 TCP 연결을 생성하는 작업은 일반적인 Valkey 또는 Redis OSS 명령보다 계산 비용이 많이 듭니다. 예를 들어 기존 연결을 재사용할 경우 SET/GET 요청을 처리하는 속도가 훨씬 빠릅니다. 크기가 한정된 클라이언트 연결 풀을 사용하면 연결 관리 시의 오버헤드가 줄어듭니다. 또한 클라이언트 애플리케이션에서 동시에 들어오는 연결 수를 제한합니다.

PHPRedis의 다음 코드 예제는 새 사용자 요청별로 새 연결이 생성된다는 것을 보여 줍니다.

```
$redis = new Redis();
if ($redis->connect($HOST, $PORT) != TRUE) {
	//ERROR: connection failed
	return;
}
$redis->set($key, $value);
unset($redis);
$redis = NULL;
```

AWS는 이 코드를 Graviton2(m6g.2xlarge) ElastiCache for Redis OSS 노드에 연결된 Amazon Elastic Compute Cloud(Amazon EC2) 인스턴스에서 루프로 벤치마킹했습니다. 클라이언트와 서버를 동일 가용 영역에 배치했습니다. 전체 작업의 평균 지연 시간은 2.82밀리초였습니다.

코드를 업데이트하고 영구 연결과 연결 풀을 사용했을 때 전체 작업의 평균 지연 시간은 0.21밀리초였습니다.

```
$redis = new Redis();
if ($redis->pconnect($HOST, $PORT) != TRUE) {
	// ERROR: connection failed
	return;
}
$redis->set($key, $value);
unset($redis);
$redis = NULL;
```

필수 redis.ini 구성:
+ `redis.pconnect.pooling_enabled=1`
+ `redis.pconnect.connection_limit=10`

다음 코드는 [Redis-py 연결 풀](https://redis.readthedocs.io/en/stable/)의 예제입니다.

```
conn = Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10))
conn.set(key, value)
```

다음 코드는 [Lettuce 연결 풀](https://lettuce.io/core/release/reference/#_connection_pooling)의 예제입니다.

```
RedisClient client = RedisClient.create(RedisURI.create(HOST, PORT));
GenericObjectPool<StatefulRedisConnection> pool = ConnectionPoolSupport.createGenericObjectPool(() -> client.connect(), new GenericObjectPoolConfig());
pool.setMaxTotal(10); // Configure max connections to 10
try (StatefulRedisConnection connection = pool.borrowObject()) {
	RedisCommands syncCommands = connection.sync();
	syncCommands.set(key, value);
}
```

# 클러스터 클라이언트 검색 및 지수 백오프(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.Redis.Discovery"></a>

클러스터 모드가 활성화된 상태에서 ElastiCache Valkey 또는 Redis OSS 클러스터에 연결하는 경우 해당 클라이언트 라이브러리가 클러스터를 인식해야 합니다. 요청을 올바른 노드로 보내고 클러스터 리디렉션을 처리하는 데 따른 성능 오버헤드를 피하려면 클라이언트가 클러스터의 해당 노드에 대한 해시 슬롯 맵을 가져와야 합니다. 따라서 클라이언트는 다음과 같은 2가지 상황에서 전체 슬롯 목록과 매핑된 노드를 검색해야 합니다.
+ 클라이언트가 초기화되었으므로 초기 슬롯 구성을 채워야 합니다.
+ MOVED 리디렉션은 서버에서 수신됩니다. 이전 프라이머리 노드가 제공하는 모든 슬롯이 복제본에서 인계되는 장애 조치이거나, 슬롯이 소스 프라이머리 노드에서 대상 프라이머리 노드로 이동할 때 샤딩이 다시 수행되는 경우가 그러한 예입니다.

클라이언트 검색은 일반적으로 Valkey 또는 Redis OSS 서버에 CLUSTER SLOT 또는 CLUSTER NODE 명령을 실행하여 수행됩니다. CLUSTER SLOT 메서드는 슬롯 범위 집합과 관련된 프라이머리 노드 및 복제본 노드를 클라이언트에 반환하므로 이 메서드를 사용하는 것이 좋습니다. 이렇게 하면 클라이언트의 추가 구문 분석이 필요하지 않아 더 효율적입니다.

클러스터 토폴로지에 따라 CLUSTER SLOT 명령의 응답 크기가 클러스터 크기를 기반으로 달라질 수 있습니다. 큰 클러스터의 노드 수가 많을수록 응답 크기도 커집니다. 따라서 클러스터 토폴로지 검색을 수행하는 클라이언트 수가 무제한으로 증가하지 않도록 하는 것이 중요합니다. 예를 들어 클라이언트 애플리케이션이 시작되거나 서버와의 연결이 끊기고 클러스터 검색을 수행해야 하는 경우, 일반적인 실수 중 하나는 클라이언트 애플리케이션이 재시도 시 지수 백오프를 추가하지 않고 여러 번 재연결 및 검색 요청을 실행하는 것입니다. 이로 인해 CPU 활용률이 100%인 상태에서 Valkey 또는 Redis OSS 서버가 장시간 응답하지 않을 수 있습니다. 각 CLUSTER SLOT 명령이 클러스터 버스의 많은 노드를 처리해야 하는 경우 중단 시간이 길어집니다. Python(redis-py-cluster) 및 Java(Lettuce 및 Redisson) 등 여러 다양한 언어에서 이러한 동작으로 인해 과거에 여러 번 클라이언트가 중단되는 경우가 있었습니다.

서버리스 캐시의 경우, 알려진 클러스터 토폴로지가 정적이고 쓰기 엔드포인트와 읽기 엔드포인트의 두 항목으로 구성되어 있으므로 많은 문제가 자동으로 완화됩니다. 또한 캐시 엔드포인트를 사용하면 클러스터 검색이 여러 노드에 자동으로 분산됩니다. 하지만 여전히 다음 권장 사항을 따르는 것이 좋습니다.

연결 및 검색 요청의 갑작스러운 유입으로 인한 영향을 완화하려면 다음 내용을 따르세요.
+ 제한된 크기의 클라이언트 연결 풀을 구현하여 클라이언트 애플리케이션에서 동시에 들어오는 연결 수를 제한합니다.
+ 제한 시간 초과로 인해 서버에서 클라이언트 연결이 끊어지면 지터가 있는 지수 백오프를 사용하여 다시 시도합니다. 이렇게 하면 여러 클라이언트가 동시에 서버에 과부하를 가하지 않도록 할 수 있습니다.
+ [ElastiCache에서 연결 엔드포인트 찾기](Endpoints.md) 섹션의 가이드를 사용하여 클러스터 검색을 수행할 클러스터 엔드포인트를 확인합니다. 이렇게 하면 클러스터에서 몇 개의 하드코딩된 시드 노드에 도달하지 않고 클러스터의 모든 노드(최대 90개)에 검색 부하를 분산할 수 있습니다.

다음은 redis-py, PHPRedis 및 Lettuce의 지수 백오프 재시도 로직에 대한 몇 가지 코드 예제입니다.

**백오프 로직 샘플 1: redis-py**

redis-py에는 실패하자마자 한 번 재시도하는 재시도 메커니즘이 내장되어 있습니다. 이 메커니즘은 [Redis OSS](https://redis.readthedocs.io/en/stable/examples/connection_examples.html#redis.Redis) 객체를 생성할 때 제공된 `retry_on_timeout` 인수를 통해 활성화할 수 있습니다. 여기서는 지수 백오프와 지터를 사용하는 사용자 지정 재시도 메커니즘을 보여 줍니다. [redis-py(\$11494)](https://github.com/andymccurdy/redis-py/pull/1494)에서 기본적으로 지수 백오프를 구현하기 위한 풀 요청이 제출된 상태입니다. 향후에는 수동으로 구현할 필요가 없을 수도 있습니다.

```
def run_with_backoff(function, retries=5):
base_backoff = 0.1 # base 100ms backoff
max_backoff = 10 # sleep for maximum 10 seconds
tries = 0
while True:
try:
  return function()
except (ConnectionError, TimeoutError):
  if tries >= retries:
	raise
  backoff = min(max_backoff, base_backoff * (pow(2, tries) + random.random()))
  print(f"sleeping for {backoff:.2f}s")
  sleep(backoff)
  tries += 1
```

그런 다음, 다음 코드를 사용하여 값을 설정할 수 있습니다.

```
client = redis.Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10))
res = run_with_backoff(lambda: client.set("key", "value"))
print(res)
```

워크로드에 따라 지연 시간에 민감한 워크로드의 기본 백오프 값을 1초에서 수십 또는 수백 밀리초로 변경하는 것을 고려할 수 있습니다.

**백오프 로직 샘플 2: PHPRedis**

PHPRedis에는 최대 10번(구성 불가능) 재시도하는 재시도 메커니즘이 내장되어 있습니다. 시도 사이에는 지연을 구성할 수 있습니다(두 번째 재시도부터 지터 사용). 자세한 내용은 다음 [샘플 예제](https://github.com/phpredis/phpredis/blob/b0b9dd78ef7c15af936144c1b17df1a9273d72ab/library.c#L335-L368)를 참조하세요. [PHPredis(\$11986)](https://github.com/phpredis/phpredis/pull/1986)에서 기본적으로 지수 백오프를 구현하기 위한 풀 요청이 제출된 상태이며, 해당 풀 요청은 병합 후 [기록](https://github.com/phpredis/phpredis/blob/develop/README.md#retry-and-backoff)되어 있습니다. PHPRedis의 최신 릴리스를 사용하는 사용자의 경우 수동으로 구현할 필요는 없지만 이전 버전을 사용하는 사용자를 위해 여기에 참조를 포함했습니다. 다음은 현재 재시도 메커니즘의 지연을 구성하는 코드 예제입니다.

```
$timeout = 0.1; // 100 millisecond connection timeout
$retry_interval = 100; // 100 millisecond retry interval
$client = new Redis();
if($client->pconnect($HOST, $PORT, $timeout, NULL, $retry_interval) != TRUE) {
	return; // ERROR: connection failed
}
$client->set($key, $value);
```

**백오프 로직 샘플 3: Lettuce**

Lettuce에는 [지수 백오프 및 Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) 관련 게시물에 명시된 지수 백오프 전략을 기반으로 하는 재시도 메커니즘이 내장되어 있습니다. 다음은 코드 발췌문은 전체적인 지터의 접근 방식을 보여 줍니다.

```
public static void main(String[] args)
{
	ClientResources resources = null;
	RedisClient client = null;

	try {
		resources = DefaultClientResources.builder()
				.reconnectDelay(Delay.fullJitter(
			Duration.ofMillis(100),     // minimum 100 millisecond delay
			Duration.ofSeconds(5),      // maximum 5 second delay
			100, TimeUnit.MILLISECONDS) // 100 millisecond base
		).build();

		client = RedisClient.create(resources, RedisURI.create(HOST, PORT));
		client.setOptions(ClientOptions.builder()
	.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(100)).build()) // 100 millisecond connection timeout
	.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(5)).build()) // 5 second command timeout
	.build());

	    // use the connection pool from above example
	} finally {
		if (connection != null) {
			connection.close();
		}

		if (client != null){
			client.shutdown();
		}

		if (resources != null){
			resources.shutdown();
		}

	}
}
```

# 클라이언트 측 제한 시간 구성(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.Redis.ClientTimeout"></a>

**클라이언트 측 제한 시간 구성**

서버가 요청을 처리하고 응답을 생성하는 데 충분한 시간을 할애할 수 있도록 클라이언트 측 제한 시간을 적절하게 구성합니다. 또한 서버와의 연결을 설정할 수 없는 경우 빠른 실패로 이어질 수 있습니다. 특정 Valkey 또는 Redis OSS 명령은 다른 명령보다 계산 비용이 더 많이 들 수 있습니다. 세부적으로 실행해야 하는 여러 명령이 포함된 Lua 스크립트 또는 MULTI/EXEC 트랜잭션을 예로 들 수 있습니다. 일반적으로 서버로부터 응답을 받기 전에 클라이언트 제한 시간이 초과되지 않도록 하려면 다음을 포함하여 클라이언트 측 제한 시간을 높게 설정하는 것이 좋습니다.
+ 여러 키에서 명령 실행
+ 여러 개별 Valkey 또는 Redis OSS 명령으로 구성된 MULTI/EXEC 트랜잭션 또는 Lua 스크립트 실행
+ 큰 값 읽기
+ 차단 작업 수행(예: BLPOP)

BLPOP 등 차단 작업의 경우 모범 사례는 명령 제한 시간을 소켓 제한 시간보다 낮은 숫자로 설정하는 것입니다.

다음은 redis-py, PHPRedis 및 Lettuce에서 클라이언트 측 제한 시간을 구현하는 코드 예제입니다.

**제한 시간 구성 샘플 1: redis-py**

다음은 redis-py를 사용한 코드 예제입니다.

```
# connect to Redis server with a 100 millisecond timeout
# give every Redis command a 2 second timeout
client = redis.Redis(connection_pool=redis.BlockingConnectionPool(host=HOST, max_connections=10,socket_connect_timeout=0.1,socket_timeout=2))

res = client.set("key", "value") # will timeout after 2 seconds
print(res)                       # if there is a connection error

res = client.blpop("list", timeout=1) # will timeout after 1 second
                                      # less than the 2 second socket timeout
print(res)
```

**제한 시간 구성 샘플 2: PHPRedis**

다음은 PHPRedis를 사용한 코드 예제입니다.

```
// connect to Redis server with a 100ms timeout
// give every Redis command a 2s timeout
$client = new Redis();
$timeout = 0.1; // 100 millisecond connection timeout
$retry_interval = 100; // 100 millisecond retry interval
$client = new Redis();
if($client->pconnect($HOST, $PORT, 0.1, NULL, 100, $read_timeout=2) != TRUE){
	return; // ERROR: connection failed
}
$client->set($key, $value);

$res = $client->set("key", "value"); // will timeout after 2 seconds
print "$res\n";                      // if there is a connection error

$res = $client->blpop("list", 1); // will timeout after 1 second
print "$res\n";                   // less than the 2 second socket timeout
```

**제한 시간 구성 샘플 3: Lettuce**

다음은 Lettuce를 사용한 코드 예제입니다.

```
// connect to Redis server and give every command a 2 second timeout
public static void main(String[] args)
{
	RedisClient client = null;
	StatefulRedisConnection<String, String> connection = null;
	try {
		client = RedisClient.create(RedisURI.create(HOST, PORT));
		client.setOptions(ClientOptions.builder()
	.socketOptions(SocketOptions.builder().connectTimeout(Duration.ofMillis(100)).build()) // 100 millisecond connection timeout
	.timeoutOptions(TimeoutOptions.builder().fixedTimeout(Duration.ofSeconds(2)).build()) // 2 second command timeout 
	.build());

		// use the connection pool from above example

		commands.set("key", "value"); // will timeout after 2 seconds
		commands.blpop(1, "list"); // BLPOP with 1 second timeout
	} finally {
		if (connection != null) {
			connection.close();
		}

		if (client != null){
			client.shutdown();
		}
	}
}
```

# 서버 측 유휴 제한 시간 구성(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.Redis.ServerTimeout"></a>

고객 애플리케이션에 연결된 유휴 클라이언트 수가 많지만 적극적으로 명령이 전송되지 않는 경우가 있습니다. 이러한 시나리오의 경우 유휴 클라이언트 수가 많으면 65,000개의 연결이 모두 소진될 수 있습니다. 이러한 시나리오가 발생하지 않도록 하려면 [Valkey 및 Redis OSS 파라미터](ParameterGroups.Engine.md#ParameterGroups.Redis)를 통해 서버의 제한 시간 설정을 적절하게 구성하세요. 이렇게 하면 서버가 유휴 클라이언트의 연결을 적극적으로 해제하여 연결 수가 증가하지 않도록 할 수 있습니다. 서버리스 캐시에서는 이 설정을 사용할 수 없습니다.

# Lua 스크립트
<a name="BestPractices.Clients.Redis.LuaScripts"></a>

Valkey 또는 Redis OSS는 Lua 스크립트 실행 명령을 포함하여 200개 이상의 명령을 지원합니다. 그러나 Lua 스크립트의 경우 Valkey 또는 Redis OSS의 메모리 및 가용성에 영향을 줄 수 있는 몇 가지 문제가 있습니다.

**파라미터화되지 않은 Lua 스크립트**

각 Lua 스크립트는 실행 전에 Valkey 또는 Redis OSS 서버에 캐시됩니다. 파라미터화되지 않은 Lua 스크립트는 고유하므로 Valkey 또는 Redis OSS 서버가 많은 수의 Lua 스크립트를 저장하고 더 많은 메모리를 사용할 수 있습니다. 이를 완화하려면 모든 Lua 스크립트가 파라미터화되었는지 확인하고 필요한 경우 정기적으로 SCRIPT FLUSH를 수행하여 캐시된 Lua 스크립트를 정리합니다.

또한 키를 제공해야 합니다. KEY 파라미터의 값이 제공되지 않으면 스크립트가 실패합니다. 예를 들어 다음과 같은 경우에는 작동하지 않습니다.

```
serverless-test-lst4hg.serverless.use1.cache.amazonaws.com:6379> eval 'return "Hello World"' 0
(error) ERR Lua scripts without any input keys are not supported.
```

이 작업은 다음과 같이 작동합니다.

```
serverless-test-lst4hg.serverless.use1.cache.amazonaws.com:6379> eval 'return redis.call("get", KEYS[1])' 1 mykey-2
"myvalue-2"
```

다음 예제는 파라미터화된 스크립트를 사용하는 방법을 보여 줍니다. 먼저 3개의 캐시된 Lua 스크립트가 생성되는 파라미터화되지 않은 접근 방식의 예제가 있으며 이 방식은 권장하지 않습니다.

```
eval "return redis.call('set','key1','1')" 0
eval "return redis.call('set','key2','2')" 0
eval "return redis.call('set','key3','3')" 0
```

대신 다음 패턴을 사용하여 전달된 파라미터를 사용할 수 있는 단일 스크립트를 생성합니다.

```
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key1 1 
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key2 2 
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 key3 3
```

**장기 실행 Lua 스크립트**

Lua 스크립트는 여러 명령을 세부적으로 실행할 수 있으므로 일반 Valkey 또는 Redis OSS 명령보다 완료하는 데 시간이 더 오래 걸릴 수 있습니다. Lua 스크립트가 읽기 전용 작업만 실행하는 경우 중간에 중지할 수 있습니다. 하지만 Lua 스크립트는 쓰기 작업을 수행하는 즉시 종료할 수 없으므로 실행을 완료해야 합니다. 장기 실행 Lua 스크립트가 변경되면 Valkey 또는 Redis OSS 서버가 오랫동안 응답하지 않을 수 있습니다. 이 문제를 완화하려면 장기 실행 Lua 스크립트를 사용하지 말고 사전 프로덕션 환경에서 스크립트를 테스트해 보세요.

**Stealth 쓰기 기능이 있는 Lua 스크립트**

Valkey 또는 Redis OSS가 `maxmemory`를 초과한 경우에도 Lua 스크립트가 Valkey 또는 Redis OSS에 새 데이터를 계속 쓸 수 있는 몇 가지 방법이 있습니다.
+ 스크립트는 Valkey 또는 Redis OSS 서버가 `maxmemory` 이하일 때 시작되며 내부에는 여러 쓰기 작업이 포함됩니다.
+ 스크립트의 첫 번째 쓰기 명령은 메모리(예: DEL)를 사용하지 않고, 이어서 메모리를 사용하는 쓰기 작업이 늘어납니다.
+ `noeviction`이 아닌 Valkey 또는 Redis OSS 서버에서 적절한 제거 정책을 구성하여 이 문제를 완화할 수 있습니다. 이렇게 하면 Redis OSS는 항목을 제거하고 Lua 스크립트 간에 메모리를 정리할 수 있습니다.

# 대규모 복합 항목 저장(Valkey 및 Redis OSS)
<a name="BestPractices.Clients.Redis.LargeItems"></a>

애플리케이션이 Valkey 또는 Redis OSS에 대규모 복합 항목을 저장하는 경우가 있습니다(예: 다중 GB 해시 데이터세트). 이렇게 하면 빈번하게 Valkey 또는 Redis OSS에서 성능 문제가 발생하므로 권장하지는 않습니다. 예를 들어 클라이언트는 HGETALL 명령을 사용하여 전체 다중 GB 해시 컬렉션을 검색할 수 있습니다. 이로 인해 클라이언트 출력 버퍼의 대규모 항목을 버퍼링하는 Valkey 또는 Redis OSS 서버에 상당한 메모리 부하가 발생할 수 있습니다. 또한 클러스터 모드의 슬롯 마이그레이션의 경우, ElastiCache는 직렬화 크기가 256MB보다 큰 항목이 포함된 슬롯은 마이그레이션하지 않습니다.

대규모 항목 문제를 해결하기 위한 권장 사항은 다음과 같습니다.
+ 대규모 복합 항목을 여러 개의 작은 항목으로 나눕니다. 예를 들어 항목 컬렉션을 식별하기 위해 키 이름에 공통 접두사를 사용하는 등 컬렉션을 적절하게 반영하는 키 이름 스키마를 사용하여 대규모 해시 컬렉션을 개별 키-값 필드로 나눕니다. 동일한 컬렉션의 여러 필드에 세부적으로 액세스해야 하는 경우 MGET 명령을 사용하여 동일한 명령에서 여러 키-값을 검색할 수 있습니다.
+ 모든 옵션을 평가했는데도 여전히 대규모 컬렉션 데이터 세트를 분리할 수 없으면 전체 컬렉션 대신 컬렉션의 데이터 하위 집합에서 작동하는 명령어를 사용해 보세요. 동일한 명령어로 전체 다중 GB 컬렉션을 세부적으로 검색해야 하는 사용 사례는 피하도록 합니다. 일례로 해시 컬렉션에서 HGETALL 대신 HGET 또는 HMGET 명령을 사용합니다.

# Lettuce 클라이언트 구성(Valkey 및 Redis OSS)
<a name="BestPractices.Clients-lettuce"></a>

이 섹션에서는 권장되는 Java 및 Lettuce 구성 옵션과 이러한 옵션을 ElastiCache 클러스터에 적용하는 방법을 설명합니다.

이 섹션의 권장 사항은 Lettuce 버전 6.2.2에서 테스트되었습니다.

**Topics**
+ [예: 클러스터 모드에 대한 Lettuce 구성, TLS 활성화됨](BestPractices.Clients-lettuce-cme.md)
+ [예: 클러스터 모드에 대한 Lettuce 구성 비활성화됨, TLS 활성화됨](BestPractices.Clients-lettuce-cmd.md)

**Java DNS 캐시 TTL**

Java 가상 머신(JVM)은 DNS 이름 조회를 캐시합니다. JVM은 호스트 이름을 IP 주소로 확인하는 경우 *Time-To-Live*(TTL)라고 하는 지정된 기간 동안 IP 주소를 캐시합니다.

TTL 값을 선택할 때는 지연 시간과 변경 사항에 대한 응답성 사이에서 절충해야 합니다. TTL이 짧을수록 DNS 해석기가 클러스터의 DNS에서 업데이트를 더 빨리 알아차릴 수 있습니다. 이렇게 하면 클러스터가 수행하는 교체 또는 기타 워크플로에 애플리케이션이 더 빠르게 응답할 수 있습니다. 그러나 TTL이 너무 낮으면 쿼리 볼륨이 증가하여 애플리케이션의 지연 시간이 늘어날 수 있습니다. TTL 값에 정답은 없지만 TTL 값을 설정할 때는 변경 사항이 적용될 때까지 기다릴 수 있는 시간을 고려하는 것이 좋습니다.

ElastiCache 노드는 변경될 수 있는 DNS 이름 항목을 사용하므로 TTL 값을 5\$110초로 짧게 설정하여 JVM을 구성하는 것이 좋습니다. 이렇게 하면 노드의 IP 주소가 변경될 때 애플리케이션이 DNS 항목을 다시 쿼리하여 리소스의 새 IP 주소를 수신하고 사용할 수 있습니다.

일부 Java 구성에서는 JVM이 다시 시작될 때까지 DNS 항목을 새로 고치지 않도록 JVM 기본 TTL이 설정되기도 합니다.

JVM TTL을 설정하는 방법에 대한 자세한 내용은 [JVM TTL을 설정하는 방법](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-jvm-ttl.html#how-to-set-the-jvm-ttl)을 참조하세요.

**Lettuce 버전**

Lettuce 버전 6.2.2 이상을 사용하는 것이 좋습니다.

**Endpoints**. 

클러스터 모드가 활성화된 클러스터를 사용하는 경우 `redisUri`를 클러스터 구성 엔드포인트로 설정합니다. 이 URI에 대한 DNS 조회는 클러스터에서 사용 가능한 모든 노드 목록을 반환하며 클러스터 초기화 중에 해당 노드 중 하나로 무작위로 확인됩니다. 토폴로지 새로 고침의 작동 방식에 대한 자세한 내용은 이 항목 뒷부분의 *dynamicRefreshResources*를 참조하세요.

**SocketOption**

[KeepAlive](https://lettuce.io/core/release/api/io/lettuce/core/SocketOptions.KeepAliveOptions.html)를 활성화합니다. 이 옵션을 활성화하면 명령 런타임 중에 실패한 연결을 처리할 필요가 줄어듭니다.

애플리케이션 요구 사항 및 워크로드에 따라 [연결 제한 시간](https://lettuce.io/core/release/api/io/lettuce/core/SocketOptions.Builder.html#connectTimeout-java.time.Duration-)을 설정해야 합니다. 자세한 내용은 이 주제의 후반부에서 연결 제한 섹션을 참조하세요.

**ClusterClientOption: 클러스터 모드가 활성화된 클라이언트 옵션**

연결이 끊어지면 [AutoReconnect](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterClientOptions.Builder.html#autoReconnect-boolean-)를 활성화합니다.

[CommandTimeout](https://lettuce.io/core/release/api/io/lettuPrce/core/RedisURI.html#getTimeout--)을 설정합니다. 자세한 내용은 이 주제의 후반부에서 연결 제한 섹션을 참조하세요.

토폴로지에서 장애가 발생한 노드를 필터링하도록 [nodeFilter](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterClientOptions.Builder.html#nodeFilter-java.util.function.Predicate-)를 설정합니다. Lettuce는 '클러스터 노드' 출력에 있는 모든 노드(PFAIL/FAIL 상태의 노드 포함)를 클라이언트의 '파티션'(샤드라고도 함)에 저장합니다. 클러스터 토폴로지를 생성하는 동안 모든 파티션 노드에 연결을 시도합니다. 장애가 발생한 노드를 추가하는 이러한 Lettuce 동작은 어떤 이유로든 노드를 교체할 때 연결 오류(또는 경고)를 유발할 수 있습니다.

예를 들어 장애 조치가 완료되고 클러스터가 복구 프로세스를 시작한 후 clusterTopology가 새로 고쳐지는 동안 다운 노드가 토폴로지에서 완전히 제거되기 전에 클러스터 버스 노드 맵에 잠시 FAIL 노드로 나열됩니다. 이 기간 동안 Lettuce 클라이언트는 이 노드를 정상 노드로 간주하고 지속적으로 연결합니다. 이로 인해 재시도가 모두 끝난 후 오류가 발생합니다.

예:

```
final ClusterClientOptions clusterClientOptions = 
    ClusterClientOptions.builder()
    ... // other options
    .nodeFilter(it -> 
        ! (it.is(RedisClusterNode.NodeFlag.FAIL) 
        || it.is(RedisClusterNode.NodeFlag.EVENTUAL_FAIL) 
        || it.is(RedisClusterNode.NodeFlag.HANDSHAKE)
        || it.is(RedisClusterNode.NodeFlag.NOADDR)))
    .validateClusterNodeMembership(false)
    .build();
redisClusterClient.setOptions(clusterClientOptions);
```

**참고**  
노드 필터링은 DynamicRefreshSources를 참으로 설정한 상태에서 사용하는 것이 가장 좋습니다. 그러지 않으면 문제가 있는 단일 시드 노드에서 토폴로지 보기를 가져와서 일부 샤드의 프라이머리 노드에 장애가 있는 것으로 간주하면 이 프라이머리 노드가 필터링되어 슬롯이 포함되지 않습니다. 여러 시드 노드가 있으면(DynamicRefreshSources가 참인 경우) 이 문제가 발생할 가능성이 줄어듭니다. 새로 승격된 프라이머리 노드로 장애 조치 후 적어도 일부 시드 노드에 업데이트된 토폴로지 보기가 있을 것이기 때문입니다.

**ClusterTopologyRefreshOptions: 클러스터 모드가 활성화된 클라이언트의 클러스터 토폴로지 새로 고침을 제어하는 옵션**

**참고**  
클러스터 모드가 비활성화된 클러스터는 클러스터 검색 명령을 지원하지 않으며 모든 클라이언트 동적 토폴로지 검색 기능과 호환되지 않습니다.  
ElastiCache 사용이 비활성화된 클러스터 모드는 Lettuce의 `MasterSlaveTopologyRefresh`와 호환되지 않습니다. 대신, 비활성화된 클러스터 모드에는 `StaticMasterReplicaTopologyProvider`를 구성하여 클러스터 읽기 및 쓰기 엔드포인트를 제공할 수 있습니다.  
클러스터 모드 비활성화 클러스터 연결에 대한 자세한 내용은 [Valkey 또는 Redis OSS(클러스터 모드 비활성화됨) 클러스터의 엔드포인트 찾기(콘솔)](Endpoints.md#Endpoints.Find.Redis)를 참조하십시오.  
Lettuce의 동적 토폴로지 검색 기능을 사용하려면 클러스터 모드 활성화 클러스터를 만들고 기존 클러스터와 샤드 구성을 동일하게 해야 합니다. 하지만 클러스터 모드 활성화 클러스터에는 최소 3개의 샤드와 1개 이상의 복제본을 구성하는 것을 권장하며, 이렇게 해야 빠른 장애 조치를 지원할 수 있습니다.

[enablePeriodicRefresh](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#enablePeriodicRefresh-java.time.Duration-)를 활성화합니다. 이렇게 하면 클라이언트가 refreshPeriod 간격(기본값: 60초)에 따라 클러스터 토폴로지를 업데이트할 수 있도록 클러스터 토폴로지를 정기적으로 업데이트할 수 있습니다. 비활성화된 경우 클라이언트는 클러스터에 대해 명령을 실행하려고 할 때 오류가 발생한 경우에만 클러스터 토폴로지를 업데이트합니다.

이 옵션을 활성화하면 이 작업을 백그라운드 작업에 추가하여 클러스터 토폴로지 새로 고침과 관련된 지연 시간을 줄일 수 있습니다. 토폴로지 새로 고침은 백그라운드 작업에서 수행되지만 노드가 많은 클러스터의 경우 다소 느릴 수 있습니다. 이는 가장 업데이트된 클러스터 보기를 얻기 위해 모든 노드에 보기를 쿼리하기 때문입니다. 대규모 클러스터를 실행하는 경우 기간을 늘리는 것이 좋습니다.

[enableAllAdaptiveRefreshTriggers](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#enableAllAdaptiveRefreshTriggers--)를 활성화합니다. 이렇게 하면 MOVED\$1REDIRECT, ASK\$1REDIRECT, PERSISTENT\$1RECONNECTS, UNCOVERED\$1SLOT, UNKNOWN\$1NODE와 같은 모든 [트리거](https://lettuce.io/core/6.1.6.RELEASE/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.RefreshTrigger.html)를 사용하는 적응형 토폴로지 새로 고침이 가능합니다. 적응형 새로 고침 트리거는 Valkey 또는 Redis OSS 클러스터 작업 중에 발생하는 이벤트를 기반으로 토폴로지 보기 업데이트를 시작합니다. 이 옵션을 활성화하면 위 트리거 중 하나가 발생할 때 즉시 토폴로지가 새로 고쳐집니다. 이벤트가 대규모로 발생할 수 있으므로 적응형 트리거 새로 고침은 제한 시간을 사용하여 속도를 제한합니다(업데이트 간 기본 제한 시간: 30).

[closeStaleConnections](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#closeStaleConnections-boolean-)를 활성화합니다. 이렇게 하면 클러스터 토폴로지를 새로 고칠 때 오래된 연결을 닫을 수 있습니다. [ClusterTopologyRefreshOptions.isPeriodicRefreshEnabled()](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.html#isPeriodicRefreshEnabled--)가 참인 경우에만 적용됩니다. 활성화되면 클라이언트는 오래된 연결을 닫고 백그라운드에서 새 연결을 만들 수 있습니다. 그러면 명령 런타임 중에 실패한 연결을 처리할 필요가 줄어듭니다.

[dynamicRefreshResources](https://lettuce.io/core/release/api/io/lettuce/core/cluster/ClusterTopologyRefreshOptions.Builder.html#dynamicRefreshSources-boolean-)를 활성화합니다. 소규모 클러스터에서는 dynamicRefreshResources를 활성화하고 대규모 클러스터에서는 비활성화하는 것이 좋습니다. dynamicRefreshResources를 활성화하면 제공된 시드 노드(예: 클러스터 구성 엔드포인트)에서 클러스터 노드를 검색할 수 있습니다. 검색된 모든 노드를 소스로 사용하여 클러스터 토폴로지를 새로 고칩니다.

동적 새로 고침을 사용하면 클러스터 토폴로지에 대해 검색된 모든 노드를 쿼리하고 가장 정확한 클러스터 보기를 선택하려고 시도합니다. 거짓으로 설정하면 초기 시드 노드만 토폴로지 검색의 소스로 사용되고 초기 시드 노드에 대한 클라이언트 수만 가져옵니다. 비활성화된 경우 클러스터 구성 엔드포인트가 장애가 발생한 노드로 확인되면 클러스터 보기를 새로 고치려는 시도가 실패하고 예외가 발생합니다. 이 시나리오는 장애가 발생한 노드의 항목이 클러스터 구성 엔드포인트에서 제거될 때까지 어느 정도 시간이 걸리기 때문에 발생할 수 있습니다. 따라서 구성 엔드포인트는 여전히 짧은 시간 동안 장애가 발생한 노드로 무작위로 확인될 수 있습니다.

하지만 활성화되면 클러스터 보기에서 수신한 모든 클러스터 노드를 사용하여 현재 보기를 쿼리합니다. 해당 보기에서 장애가 발생한 노드를 필터링하므로 토폴로지 새로 고침이 성공적으로 이루어집니다. 그러나 dynamicRefreshSources가 참인 경우 Lettuce는 모든 노드를 쿼리하여 클러스터 보기를 가져온 다음, 결과를 비교합니다. 따라서 노드가 많은 클러스터의 경우 비용이 많이 들 수 있습니다. 노드가 많은 클러스터의 경우 이 기능을 끄는 것이 좋습니다.

```
final ClusterTopologyRefreshOptions topologyOptions = 
    ClusterTopologyRefreshOptions.builder()
    .enableAllAdaptiveRefreshTriggers()
    .enablePeriodicRefresh()
    .dynamicRefreshSources(true)
    .build();
```

**ClientResources**

[DnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DefaultClientResources.Builder.html#dnsResolver-io.lettuce.core.resource.DnsResolver-)를 [DirContextDnsResolver](https://lettuce.io/core/release/api/io/lettuce/core/resource/DirContextDnsResolver.html)를 사용하여 구성합니다. DNS 해석기는 Java의 com.sun.jndi.dns.DnsContextFactory를 기반으로 합니다.

지수 백오프 및 풀 지터를 사용하여 [reconnectDelay](https://lettuce.io/core/release/api/io/lettuce/core/resource/DefaultClientResources.Builder.html#reconnectDelay-io.lettuce.core.resource.Delay-)를 구성합니다. Lettuce에는 지수 백오프 전략을 기반으로 하는 재시도 메커니즘이 내장되어 있습니다. 자세한 내용은 AWS 아키텍처 블로그의 [지수 백오프 및 지터](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter)를 참조하세요. 재시도 백오프 전략의 중요성에 대한 자세한 내용은 AWS 데이터베이스 블로그의 [모범 사례 블로그 게시물](https://aws.amazon.com/blogs/database/best-practices-redis-clients-and-amazon-elasticache-for-redis/)에서 백오프 논리 섹션을 참조하세요.

```
ClientResources clientResources = DefaultClientResources.builder()
   .dnsResolver(new DirContextDnsResolver())
    .reconnectDelay(
        Delay.fullJitter(
            Duration.ofMillis(100),     // minimum 100 millisecond delay
            Duration.ofSeconds(10),      // maximum 10 second delay
            100, TimeUnit.MILLISECONDS)) // 100 millisecond base
    .build();
```

**제한 시간**

명령 제한 시간보다 낮은 연결 제한 시간 값을 사용합니다. Lettuce는 지연 연결 설정을 사용합니다. 따라서 연결 제한 시간이 명령 제한 시간보다 긴 경우 Lettuce가 비정상 노드에 연결하려고 시도하고 명령 제한 시간이 항상 초과하면 토폴로지 새로 고침 후에도 오류가 계속 발생하는 기간이 있을 수 있습니다.

서로 다른 명령에 동적 명령 제한 시간을 사용합니다. 명령에 기대되는 기간에 따라 명령 제한 시간을 설정하는 것이 좋습니다. 예를 들어 FLUSHDB, FLUSHALL, KEYS, SMEMBERS 또는 Lua 스크립트와 같은 여러 키를 반복하는 명령에는 더 긴 제한 시간을 사용하세요. SET, GET 및 HSET과 같은 단일 키 명령에는 더 짧은 제한 시간을 사용합니다.

**참고**  
다음 예시에서 구성된 제한 시간은 최대 20바이트 길이의 키와 값으로 SET/GET 명령을 실행한 테스트용입니다. 명령이 복잡하거나 키와 값이 크면 처리 시간이 더 오래 걸릴 수 있습니다. 애플리케이션의 사용 사례에 따라 제한 시간을 설정해야 합니다.

```
private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);
    
SocketOptions socketOptions = SocketOptions.builder()
    .connectTimeout(CONNECT_TIMEOUT)
    .build();
 

class DynamicClusterTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.CLUSTER)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration defaultCommandTimeout;
    private final Duration metaCommandTimeout;

    DynamicClusterTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

// Use a dynamic timeout for commands, to avoid timeouts during
// cluster management and slow operations.
TimeoutOptions timeoutOptions = TimeoutOptions.builder()
.timeoutSource(
    new DynamicClusterTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
.build();
```

# 예: 클러스터 모드에 대한 Lettuce 구성, TLS 활성화됨
<a name="BestPractices.Clients-lettuce-cme"></a>

**참고**  
다음 예시의 제한 시간은 최대 20바이트 길이의 키와 값으로 SET/GET 명령을 실행한 테스트용입니다. 명령이 복잡하거나 키와 값이 크면 처리 시간이 더 오래 걸릴 수 있습니다. 애플리케이션의 사용 사례에 따라 제한 시간을 설정해야 합니다.

```
// Set DNS cache TTL
public void setJVMProperties() {
    java.security.Security.setProperty("networkaddress.cache.ttl", "10");
}

private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);

// Create RedisURI from the cluster configuration endpoint
clusterConfigurationEndpoint = <cluster-configuration-endpoint> // TODO: add your cluster configuration endpoint
final RedisURI redisUriCluster =
    RedisURI.Builder.redis(clusterConfigurationEndpoint)
        .withPort(6379)
        .withSsl(true)
        .build();

// Configure the client's resources                
ClientResources clientResources = DefaultClientResources.builder()
    .reconnectDelay(
        Delay.fullJitter(
            Duration.ofMillis(100),     // minimum 100 millisecond delay
            Duration.ofSeconds(10),      // maximum 10 second delay
            100, TimeUnit.MILLISECONDS)) // 100 millisecond base
    .dnsResolver(new DirContextDnsResolver())
    .build(); 

// Create a cluster client instance with the URI and resources
RedisClusterClient redisClusterClient = 
    RedisClusterClient.create(clientResources, redisUriCluster);

// Use a dynamic timeout for commands, to avoid timeouts during
// cluster management and slow operations.
class DynamicClusterTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.CLUSTER)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration metaCommandTimeout;
    private final Duration defaultCommandTimeout;

    DynamicClusterTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

TimeoutOptions timeoutOptions = TimeoutOptions.builder()
    .timeoutSource(new DynamicClusterTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
     .build();

// Configure the topology refreshment options
final ClusterTopologyRefreshOptions topologyOptions = 
    ClusterTopologyRefreshOptions.builder()
    .enableAllAdaptiveRefreshTriggers()
    .enablePeriodicRefresh()
    .dynamicRefreshSources(true)
    .build();

// Configure the socket options
final SocketOptions socketOptions = 
    SocketOptions.builder()
    .connectTimeout(CONNECT_TIMEOUT) 
    .keepAlive(true)
    .build();

// Configure the client's options
final ClusterClientOptions clusterClientOptions = 
    ClusterClientOptions.builder()
    .topologyRefreshOptions(topologyOptions)
    .socketOptions(socketOptions)
    .autoReconnect(true)
    .timeoutOptions(timeoutOptions) 
    .nodeFilter(it -> 
        ! (it.is(RedisClusterNode.NodeFlag.FAIL) 
        || it.is(RedisClusterNode.NodeFlag.EVENTUAL_FAIL) 
        || it.is(RedisClusterNode.NodeFlag.NOADDR))) 
    .validateClusterNodeMembership(false)
    .build();
    
redisClusterClient.setOptions(clusterClientOptions);

// Get a connection
final StatefulRedisClusterConnection<String, String> connection = 
    redisClusterClient.connect();

// Get cluster sync/async commands   
RedisAdvancedClusterCommands<String, String> sync = connection.sync();
RedisAdvancedClusterAsyncCommands<String, String> async = connection.async();
```

# 예: 클러스터 모드에 대한 Lettuce 구성 비활성화됨, TLS 활성화됨
<a name="BestPractices.Clients-lettuce-cmd"></a>

**참고**  
다음 예시의 제한 시간은 최대 20바이트 길이의 키와 값으로 SET/GET 명령을 실행한 테스트용입니다. 명령이 복잡하거나 키와 값이 크면 처리 시간이 더 오래 걸릴 수 있습니다. 애플리케이션의 사용 사례에 따라 제한 시간을 설정해야 합니다.

```
// Set DNS cache TTL
public void setJVMProperties() {
    java.security.Security.setProperty("networkaddress.cache.ttl", "10");
}

private static final Duration META_COMMAND_TIMEOUT = Duration.ofMillis(1000);
private static final Duration DEFAULT_COMMAND_TIMEOUT = Duration.ofMillis(250);
// Socket connect timeout should be lower than command timeout for Lettuce
private static final Duration CONNECT_TIMEOUT = Duration.ofMillis(100);

// Create RedisURI from the primary/reader endpoint
clusterEndpoint = <primary/reader-endpoint> // TODO: add your node endpoint
RedisURI redisUriStandalone =
    RedisURI.Builder.redis(clusterEndpoint).withPort(6379).withSsl(true).withDatabase(0).build();

ClientResources clientResources =
    DefaultClientResources.builder()
        .dnsResolver(new DirContextDnsResolver())
        .reconnectDelay(
            Delay.fullJitter(
                Duration.ofMillis(100), // minimum 100 millisecond delay
                Duration.ofSeconds(10), // maximum 10 second delay
                100,
                TimeUnit.MILLISECONDS)) // 100 millisecond base
        .build();

// Use a dynamic timeout for commands, to avoid timeouts during
// slow operations.
class DynamicTimeout extends TimeoutSource {
     private static final Set<ProtocolKeyword> META_COMMAND_TYPES = ImmutableSet.<ProtocolKeyword>builder()
          .add(CommandType.FLUSHDB)
          .add(CommandType.FLUSHALL)
          .add(CommandType.INFO)
          .add(CommandType.KEYS)
          .build();

    private final Duration metaCommandTimeout;
    private final Duration defaultCommandTimeout;

    DynamicTimeout(Duration defaultTimeout, Duration metaTimeout)
    {
        defaultCommandTimeout = defaultTimeout;
        metaCommandTimeout = metaTimeout;
    }

    @Override
    public long getTimeout(RedisCommand<?, ?, ?> command) {
        if (META_COMMAND_TYPES.contains(command.getType())) {
            return metaCommandTimeout.toMillis();
        }
        return defaultCommandTimeout.toMillis();
    }
}

TimeoutOptions timeoutOptions = TimeoutOptions.builder()
    .timeoutSource(new DynamicTimeout(DEFAULT_COMMAND_TIMEOUT, META_COMMAND_TIMEOUT))
     .build();                      
                                    
final SocketOptions socketOptions =
    SocketOptions.builder().connectTimeout(CONNECT_TIMEOUT).keepAlive(true).build();

ClientOptions clientOptions =
    ClientOptions.builder().timeoutOptions(timeoutOptions).socketOptions(socketOptions).build();

RedisClient redisClient = RedisClient.create(clientResources, redisUriStandalone);
redisClient.setOptions(clientOptions);
```

## 듀얼 스택 클러스터에 선호되는 프로토콜 구성(Valkey 및 Redis OSS)
<a name="network-type-configuring-dual-stack-redis"></a>

클러스터 모드가 활성화된 Valkey 또는 Redis OSS 클러스터의 경우, 클라이언트가 클러스터 내 노드에 연결하는 데 사용할 프로토콜을 IP Discovery 파라미터로 제어할 수 있습니다. IP Discovery 파라미터는 IPv4 또는 IPv6로 설정할 수 있습니다.

Valkey 또는 Redis OSS 클러스터의 경우, IP Discovery 파라미터가 [cluster slots ()](https://valkey.io/commands/cluster-slots/), [cluster shards ()](https://valkey.io/commands/cluster-shards/), [cluster nodes ()](https://valkey.io/commands/cluster-nodes/) 출력에 사용되는 IP 프로토콜을 설정합니다. 이러한 명령은 클라이언트가 클러스터 토폴로지를 검색하는 데 사용됩니다. 클라이언트는 이들 명령 내의 IP를 사용하여 클러스터 내 다른 노드에 연결합니다.

IP Discovery를 변경해도 연결된 클라이언트는 가동 중지되지 않습니다. 하지만 변경 사항이 전파되려면 다소 시간이 소요됩니다. Valkey 또는 Redis OSS 클러스터에 변경 사항이 완전히 전파된 시점을 확인하려면 `cluster slots`의 출력을 모니터링하면 됩니다. 클러스터 슬롯 명령에서 반환된 모든 노드가 새 프로토콜이 있는 IP를 보고하면, 변경 사항이 완전히 전파된 것입니다.

Redis-Py 사용 예제:

```
cluster = RedisCluster(host="xxxx", port=6379)
target_type = IPv6Address # Or IPv4Address if changing to IPv4

nodes = set()
while len(nodes) == 0 or not all((type(ip_address(host)) is target_type) for host in nodes):
    nodes = set()

   # This refreshes the cluster topology and will discovery any node updates.
   # Under the hood it calls cluster slots
    cluster.nodes_manager.initialize()
    for node in cluster.get_nodes():
        nodes.add(node.host)
    self.logger.info(nodes)

    time.sleep(1)
```

Lettuce 사용 예제:

```
RedisClusterClient clusterClient = RedisClusterClient.create(RedisURI.create("xxxx", 6379));

Class targetProtocolType = Inet6Address.class; // Or Inet4Address.class if you're switching to IPv4

Set<String> nodes;
    
do {
   // Check for any changes in the cluster topology.
   // Under the hood this calls cluster slots
    clusterClient.refreshPartitions();
    Set<String> nodes = new HashSet<>();

    for (RedisClusterNode node : clusterClient.getPartitions().getPartitions()) {
        nodes.add(node.getUri().getHost());
    }

    Thread.sleep(1000);
} while (!nodes.stream().allMatch(node -> {
            try {
                return finalTargetProtocolType.isInstance(InetAddress.getByName(node));
            } catch (UnknownHostException ignored) {}
            return false;
}));
```