lettuce redis 클라이언트
TL;DR
- lettuce redis 클라이언트의 핵심 개념과 적용 범위를 정리
- 등장 배경과 필요한 이유를 요약
- 주요 특징과 실무 활용 포인트를 정리
1. 개념
lettuce redis 클라이언트의 핵심 개념과 범위를 간단히 정의하고, 왜 이 문서가 필요한지 요점을 잡습니다.
2. 배경
이 주제가 등장하게 된 배경과 문제 상황, 기술적 맥락을 짚습니다.
3. 이유
왜 이 접근이 필요한지, 기존 대안의 한계나 목표를 설명합니다.
4. 특징
문서에서 다루는 주요 구성요소와 실전 적용 포인트를 정리합니다.
5. 상세 내용
Lettuce Redis 클라이언트
작성일: 2026-03-04 카테고리: Backend / Java / Spring / Redis 포함 내용: Lettuce, Jedis, Redisson, Netty, 비동기 I/O, 논블로킹, 커넥션 풀, StatefulRedisConnection, RedisFuture, Project Reactor, Redis Cluster, Sentinel, Pub/Sub, Redis Streams, ReadFrom, Pipeline, MULTI/EXEC, EventLoop, TCP KeepAlive, Valkey, Spring Data Redis, LettuceConnectionFactory
1. Lettuce란?
핵심 정의
┌─────────────────────────────────────────────────────────────────┐
│ Lettuce 핵심 개념 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Lettuce = Java로 작성된 Redis 클라이언트 라이브러리 │
│ Netty 기반 비동기/논블로킹 I/O │
│ Spring Boot 2.0+ 기본 Redis 클라이언트 │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Spring Boot Application │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ Spring Data Redis │ │ │
│ │ │ ┌──────────────────────────────────────────┐ │ │ │
│ │ │ │ Lettuce (기본 클라이언트) │ │ │ │
│ │ │ │ ┌──────────────────────────────────┐ │ │ │ │
│ │ │ │ │ Netty (비동기 I/O 프레임워크) │ │ │ │ │
│ │ │ │ └──────────────────────────────────┘ │ │ │ │
│ │ │ └──────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Redis Server │ │
│ └──────────────┘ │
│ │
│ 3가지 클라이언트 비교: │
│ ┌────────────┬──────────────────────────────────────────────┐ │
│ │ Lettuce │ 비동기/논블로킹, Netty 기반, 커넥션 공유 가능 │ │
│ ├────────────┼──────────────────────────────────────────────┤ │
│ │ Jedis │ 동기/블로킹, 커넥션 풀 필수, 더 단순 │ │
│ ├────────────┼──────────────────────────────────────────────┤ │
│ │ Redisson │ 분산 객체 플랫폼, 분산 락/컬렉션 제공 │ │
│ └────────────┴──────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 등장 배경: 왜 Jedis 대신 Lettuce인가?
Jedis의 근본적 한계
┌─────────────────────────────────────────────────────────────────┐
│ Jedis의 아키텍처 문제점 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 문제 1: 스레드 비안전 (Thread-Unsafe) │
│ │
│ Jedis 인스턴스 1개 = TCP 소켓 1개 = 스레드 1개 전용 │
│ │
│ Thread-A ──SET "key" "val"──┐ │
│ ├──→ 같은 소켓 → RESP 스트림 오염! │
│ Thread-B ──GET "other"──────┘ │
│ │
│ 결과: "expected '$' but got ' '" 프로토콜 에러 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 2: 블로킹 I/O │
│ │
│ Thread ──sendCommand()──→ Redis │
│ Thread ──[대기 중...]────← 응답 올 때까지 블로킹 │
│ Thread ──getReply()──────← 응답 수신 │
│ │
│ → 응답 대기 중 스레드가 아무 일도 못 함 │
│ → 동시성 = 커넥션 풀의 커넥션 수에 비례 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 3: 비동기/리액티브 미지원 │
│ │
│ Jedis API: 동기만 지원 (Pipeline 제외) │
│ → Spring WebFlux와 통합 불가 │
│ → Reactive 스택에서 사용 시 스레드 낭비 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 4: Redis Cluster에서 비동기 미지원 │
│ 문제 5: 릴리즈 지연 심각 (Redis 신 버전 기능 미지원) │
│ │
└─────────────────────────────────────────────────────────────────┘
Spring Boot 2.0에서 Lettuce로 교체된 경위
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot 기본 Redis 클라이언트 교체 이력 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Spring Boot 1.x: Jedis (기본값) │
│ │
│ 2017년: Mark Paluch (Lettuce 메인테이너)가 │
│ Spring Boot GitHub Issue #10480 제출 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ "The arrangement with Jedis suffers from changes │ │
│ │ in the Jedis core development. Features of newer │ │
│ │ Redis versions are not supported. Requests for a │ │
│ │ new release were not completed in a timely manner." │ │
│ │ — Mark Paluch, Issue #10480 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ Andy Wilkinson (Spring 팀)이 동의 │
│ → Spring Boot 2.0.0.M5에서 Lettuce로 교체 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 2023년: Redis 7.2 릴리즈 │
│ → Lettuce가 Redis 공식 클라이언트 패밀리로 지정 │
│ → 라이선스: Apache 2.0 → MIT로 변경 │
│ → GitHub: lettuce-io/lettuce-core → redis/lettuce 이관 │
│ │
│ Lettuce 개발 연혁: │
│ 2011년: wuqke가 최초 개발 │
│ 2014년: Mark Paluch (mp911de)가 프로젝트 인수 │
│ 2018년: Spring Boot 2.0 기본 클라이언트 │
│ 2023년: Redis 공식 클라이언트 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 내부 아키텍처
Netty 기반 비동기 I/O 모델
┌─────────────────────────────────────────────────────────────────┐
│ Lettuce 내부 아키텍처 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Jedis 모델] │
│ │
│ Thread-A → Conn-1 → Redis 각 스레드가 전용 커넥션 점유 │
│ Thread-B → Conn-2 → Redis 커넥션 풀 필수 │
│ Thread-C → Conn-3 → Redis 풀 소진 시 대기 or 에러 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Lettuce 모델] │
│ │
│ Thread-A ─┐ │
│ Thread-B ─┼──→ 단일 StatefulRedisConnection ──→ Redis │
│ Thread-C ─┘ (Netty EventLoop가 멀티플렉싱) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 상세 흐름: │
│ │
│ Application Threads │
│ Thread-A ─┐ │
│ Thread-B ─┼─→ WriteTask를 EventLoop 큐에 enqueue │
│ Thread-C ─┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Netty EventLoop (단일 스레드) │ │
│ │ │ │
│ │ CommandHandler │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Command Queue (in-flight) │ │ │
│ │ │ [cmd1, cmd2, cmd3, ...] │ │ │
│ │ │ 각 cmd에 RedisFuture 연결 │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │
│ └───────────┼──────────────────────────────────────┘ │
│ │ 단일 TCP Channel │
│ ▼ │
│ Redis Server │
│ │ │
│ ← RESP 응답 ← │
│ │ │
│ EventLoop가 응답을 Queue의 cmd와 매핑 │
│ → 각 Thread의 RedisFuture.complete() │
│ │
└─────────────────────────────────────────────────────────────────┘
3가지 API 레이어
// 같은 StatefulRedisConnection에서 세 가지 API 획득
StatefulRedisConnection<String, String> conn = redisClient.connect();
// 1. 동기 API — 내부적으로 async().get() 블로킹
RedisCommands<String, String> sync = conn.sync();
String val = sync.get("key"); // 블로킹
// 2. 비동기 API — RedisFuture (CompletableFuture 구현체)
RedisAsyncCommands<String, String> async = conn.async();
RedisFuture<String> future = async.get("key");
future.thenAccept(System.out::println);
// 3. 리액티브 API — Project Reactor (Mono/Flux)
RedisReactiveCommands<String, String> reactive = conn.reactive();
Mono<String> mono = reactive.get("key");
mono.subscribe(System.out::println);
┌─────────────────────────────────────────────────────────────────┐
│ API 레이어별 사용 시나리오 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┬──────────────────────────────────────────────┐ │
│ │ sync() │ Spring MVC, 단순 CRUD │ │
│ │ │ 기존 동기 코드 호환 │ │
│ │ │ 내부적으로 Future.get() 블로킹 │ │
│ ├────────────┼──────────────────────────────────────────────┤ │
│ │ async() │ 여러 Redis 호출을 병렬 실행할 때 │ │
│ │ │ CompletableFuture 체이닝 │ │
│ │ │ RedisFuture<V> 반환 │ │
│ ├────────────┼──────────────────────────────────────────────┤ │
│ │ reactive() │ Spring WebFlux, 리액티브 스택 │ │
│ │ │ Project Reactor의 Mono/Flux │ │
│ │ │ Backpressure 지원 │ │
│ └────────────┴──────────────────────────────────────────────┘ │
│ │
│ 핵심: 세 API 모두 같은 커넥션을 공유한다! │
│ → RedisClient.connect() 한 번이면 충분 │
│ │
└─────────────────────────────────────────────────────────────────┘
커넥션 공유가 가능한 이유
┌─────────────────────────────────────────────────────────────────┐
│ 왜 단일 커넥션을 여러 스레드가 공유할 수 있는가? │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1) Netty EventLoop가 쓰기/읽기를 직렬화 │
│ → 여러 스레드의 명령이 큐를 통해 순차 처리 │
│ → synchronized 블록 불필요 │
│ │
│ 2) Redis 프로토콜이 요청/응답 순서를 보장 │
│ → cmd1, cmd2, cmd3 순서로 보내면 │
│ → resp1, resp2, resp3 순서로 돌아옴 │
│ → 각 RedisFuture에 정확히 매핑 가능 │
│ │
│ 3) 명령별 고유 RedisFuture 객체 │
│ → 다른 스레드의 응답과 절대 섞이지 않음 │
│ │
│ ⚠️ 예외: 커넥션 공유 불가능한 경우 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ BLPOP, BRPOP, XREAD (blocking) │ │
│ │ → 응답이 올 때까지 커넥션 점유 │ │
│ │ → 다른 명령이 큐에서 대기 │ │
│ │ → 전용 커넥션 필요 │ │
│ │ │ │
│ │ MULTI / EXEC (트랜잭션) │ │
│ │ → MULTI 상태가 커넥션에 바인딩 │ │
│ │ → 다른 스레드의 명령이 트랜잭션에 섞임 │ │
│ │ → 전용 커넥션 필요 │ │
│ │ │ │
│ │ WATCH │ │
│ │ → 키 감시 상태가 커넥션에 바인딩 │ │
│ │ → 전용 커넥션 필요 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4. Lettuce vs Jedis 심층 비교
아키텍처 비교
┌─────────────────────────────────────────────────────────────────┐
│ Jedis vs Lettuce 내부 구조 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Jedis 클래스 계층] │
│ │
│ Jedis (String API) │
│ └── BinaryJedis (byte[] 기반, 5,061줄) │
│ └── Client (Connection 래퍼) │
│ └── Connection (Socket I/O) │
│ └── Protocol (RESP 직렬화) │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ // Jedis 내부 - 모든 명령의 패턴 │ │
│ │ public Long rpush(byte[] key, byte[]... args) { │ │
│ │ client.rpush(key, args); // 소켓 write │ │
│ │ return client.getIntegerReply(); // 블로킹 │ │
│ │ } │ │
│ │ // write와 read 사이에 다른 스레드가 끼어들면 │ │
│ │ // → RESP 스트림 오염! │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Lettuce 클래스 계층] │
│ │
│ RedisClient (Singleton, Netty 리소스 관리) │
│ └── StatefulRedisConnection<K,V> (Thread-safe) │
│ ├── sync() → RedisCommands<K,V> │
│ ├── async() → RedisAsyncCommands<K,V> │
│ └── reactive() → RedisReactiveCommands<K,V> │
│ │
│ Javadoc: "A thread-safe connection to a Redis server. │
│ Multiple threads may share one StatefulRedisConnection." │
│ │
└─────────────────────────────────────────────────────────────────┘
전체 비교 표
┌────────────────┬──────────────────────┬──────────────────────┐
│ 항목 │ Jedis │ Lettuce │
├────────────────┼──────────────────────┼──────────────────────┤
│ I/O 모델 │ 블로킹 (BIO) │ 논블로킹 (Netty NIO) │
│ 스레드 안전 │ 비안전 (풀 필수) │ 안전 (공유 가능) │
│ API │ 동기만 │ 동기+비동기+리액티브 │
│ 커넥션 모델 │ 스레드당 전용 커넥션 │ 단일 커넥션 공유 │
│ 커넥션 풀 │ 필수 (JedisPool) │ 선택적 │
│ Cluster 비동기 │ 미지원 (동기만) │ 지원 │
│ Reactive │ 미지원 │ Project Reactor │
│ Pub/Sub │ 전용 스레드 필요 │ EventLoop 통합 │
│ 자동 재연결 │ 제한적 │ ConnectionWatchdog │
│ 의존성 │ 경량 (자체 구현) │ Netty (무거움) │
│ 학습 곡선 │ 낮음 │ 중간 │
│ 기본 클라이언트│ Spring Boot 1.x │ Spring Boot 2.0+ │
└────────────────┴──────────────────────┴──────────────────────┘
성능 벤치마크
┌─────────────────────────────────────────────────────────────────┐
│ JMH 벤치마크 결과 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Sentinel 환경, 100 스레드, 1M 키, 5KB 데이터] │
│ │
│ ┌──────────────────────┬───────────────┐ │
│ │ 벤치마크 │ ops/ms │ │
│ ├──────────────────────┼───────────────┤ │
│ │ Jedis GET │ 17.858 │ │
│ │ Jedis SET │ 143.064 │ │
│ │ Lettuce Async GET │ 12.284 │ │
│ │ Lettuce Async SET │ 131.441 │ │
│ │ Lettuce Reactive GET │ 12.299 │ │
│ └──────────────────────┴───────────────┘ │
│ │
│ → 단일 커넥션 + 낮은 동시성: Jedis가 약간 우위 │
│ → 이유: Jedis는 여러 커넥션 사용 / Lettuce는 단일 커넥션 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Cluster 환경, 6 노드] │
│ │
│ ┌──────────────────────┬───────────────┐ │
│ │ 벤치마크 │ ops/ms │ │
│ ├──────────────────────┼───────────────┤ │
│ │ Lettuce Async SET │ 220.132 │ ← 압도적 │
│ │ Lettuce Reactive GET │ 19.598 │ │
│ │ Jedis SET │ 150.455 │ │
│ │ Jedis GET │ 18.842 │ │
│ └──────────────────────┴───────────────┘ │
│ │
│ → 클러스터 + 높은 동시성: Lettuce Async가 46% 더 빠름 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 핵심 해석: │
│ ├── 단순 동기 + 낮은 동시성 → Jedis가 약간 유리 │
│ ├── 높은 동시성 + Cluster → Lettuce Async 압도적 우위 │
│ ├── Lettuce 배치 모드 → 처리량 5배 향상 (~100K→500K ops/s) │
│ └── JedisPool 200개 초과 시 응답 시간 급격히 증가 │
│ │
└─────────────────────────────────────────────────────────────────┘
5. Spring Data Redis에서의 Lettuce 설정
기본 의존성
<!-- Spring Boot starter에 Lettuce 자동 포함 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 커넥션 풀 사용 시 필수 (MULTI/EXEC, 블로킹 명령) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- TCP KeepAlive/Timeout 사용 시 (Linux) -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<classifier>linux-x86_64</classifier>
</dependency>
application.yml 설정
spring:
data:
redis:
host: localhost
port: 6379
password: yourpassword
timeout: 30s # 커맨드 타임아웃 (기본 60s)
database: 0
lettuce:
pool:
max-active: 8 # 최대 활성 커넥션 (기본 8)
max-idle: 8 # 최대 유휴 커넥션
min-idle: 2 # 최소 유휴 커넥션
max-wait: 3000ms # 커넥션 획득 최대 대기
time-between-eviction-runs: 30s
Java Configuration
@Configuration
public class RedisConfig {
// ===== 기본 Standalone 설정 =====
@Bean
public LettuceConnectionFactory connectionFactory() {
RedisStandaloneConfiguration serverConfig =
new RedisStandaloneConfiguration("localhost", 6379);
serverConfig.setPassword("yourpassword");
// TCP 수준 타임아웃 설정 (dead connection 조기 감지)
SocketOptions socketOptions = SocketOptions.builder()
.keepAlive(SocketOptions.KeepAliveOptions.builder()
.idle(Duration.ofSeconds(5)) // TCP_KEEPIDLE
.interval(Duration.ofSeconds(5)) // TCP_KEEPINTVL
.count(3) // TCP_KEEPCNT
.enable()
.build())
.tcpUserTimeout(SocketOptions.TcpUserTimeoutOptions.builder()
.tcpUserTimeout(Duration.ofSeconds(20))
.enable()
.build())
.build();
LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(30))
.clientOptions(ClientOptions.builder()
.socketOptions(socketOptions)
.disconnectedBehavior(
ClientOptions.DisconnectedBehavior.REJECT_COMMANDS)
.autoReconnect(true)
.build())
.build();
return new LettuceConnectionFactory(serverConfig, clientConfig);
}
// ===== Cluster 설정 =====
@Bean
public LettuceConnectionFactory clusterConnectionFactory() {
RedisClusterConfiguration clusterConfig =
new RedisClusterConfiguration(
List.of("node1:7001", "node2:7002", "node3:7003"));
clusterConfig.setMaxRedirects(3);
ClusterTopologyRefreshOptions topologyRefresh =
ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers()
.enablePeriodicRefresh(Duration.ofSeconds(60))
.build();
LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.clientOptions(ClusterClientOptions.builder()
.topologyRefreshOptions(topologyRefresh)
.autoReconnect(true)
.build())
.commandTimeout(Duration.ofSeconds(30))
.build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
// ===== Sentinel 설정 =====
@Bean
public LettuceConnectionFactory sentinelConnectionFactory() {
RedisSentinelConfiguration sentinelConfig =
new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("sentinel1", 26379)
.sentinel("sentinel2", 26380)
.sentinel("sentinel3", 26381);
sentinelConfig.setPassword("redis-password");
sentinelConfig.setSentinelPassword("sentinel-password");
return new LettuceConnectionFactory(sentinelConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(
new GenericJackson2JsonRedisSerializer());
return template;
}
}
6. 고급 기능
파이프라이닝 (Pipelining)
┌─────────────────────────────────────────────────────────────────┐
│ Jedis vs Lettuce 파이프라이닝 차이 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Jedis Pipeline] │
│ 명시적 버퍼링 → sync() 호출 시 한 번에 전송 │
│ │
│ try (Pipeline pipe = jedis.pipelined()) { │
│ Response<String> r1 = pipe.set("k1", "v1"); // 버퍼에 축적 │
│ Response<String> r2 = pipe.get("k1"); // 버퍼에 축적 │
│ pipe.sync(); // ← 여기서 한 번에 flush + 응답 수신 │
│ r2.get(); // sync() 이후에만 접근 가능 │
│ } │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ [Lettuce Pipeline] │
│ 기본적으로 auto-flush (자동 파이프라이닝) │
│ 각 명령이 즉시 TCP로 전송되지만 응답을 기다리지 않음 │
│ │
│ // 자동 파이프라이닝 (기본 동작) │
│ RedisFuture<String> f1 = async.set("k1", "v1"); // 즉시 전송 │
│ RedisFuture<String> f2 = async.get("k1"); // 즉시 전송 │
│ // Netty가 자동으로 파이프라이닝 │
│ │
│ // 수동 배치 모드 (처리량 최대 5배 향상) │
│ async.setAutoFlushCommands(false); │
│ List<RedisFuture<?>> futures = new ArrayList<>(); │
│ for (int i = 0; i < 1000; i++) { │
│ futures.add(async.set("k" + i, "v" + i)); │
│ } │
│ async.flushCommands(); // 한 번에 전송 │
│ LettuceFutures.awaitAll(5, SECONDS, futures...); │
│ async.setAutoFlushCommands(true); // 복원 │
│ │
│ ⚠️ setAutoFlushCommands(false)는 │
│ 멀티스레드 환경에서 레이스 컨디션 위험! │
│ │
└─────────────────────────────────────────────────────────────────┘
Pub/Sub
// 리스너 기반 (명령형)
StatefulRedisPubSubConnection<String, String> pubSub =
client.connectPubSub();
pubSub.addListener(new RedisPubSubAdapter<>() {
@Override
public void message(String channel, String message) {
// ⚠️ EventLoop에서 실행됨 → 블로킹 금지!
processAsync(message);
}
});
pubSub.sync().subscribe("my-channel");
// 리액티브 Pub/Sub (Flux 기반)
RedisPubSubReactiveCommands<String, String> reactive =
pubSub.reactive();
reactive.subscribe("events:*").subscribe();
reactive.observeChannels()
.filter(msg -> msg.getChannel().startsWith("events:"))
.map(ChannelMessage::getMessage)
.subscribe(this::process);
클러스터 토폴로지 자동 갱신
┌─────────────────────────────────────────────────────────────────┐
│ Cluster Adaptive Topology Refresh 트리거 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┬───────────────────────────────────┐ │
│ │ 트리거 │ 설명 │ │
│ ├──────────────────────┼───────────────────────────────────┤ │
│ │ MOVED_REDIRECT │ MOVED 에러 수신 시 갱신 │ │
│ │ ASK_REDIRECT │ ASK 에러 수신 시 갱신 │ │
│ │ PERSISTENT_RECONNECTS│ 지속적 재연결 실패 시 갱신 │ │
│ │ UNKNOWN_NODE (5.1+) │ 알 수 없는 노드 발견 시 갱신 │ │
│ │ UNCOVERED_SLOT (5.2+)│ 커버되지 않는 슬롯 발견 시 갱신 │ │
│ └──────────────────────┴───────────────────────────────────┘ │
│ │
│ ⚠️ 기본값은 비활성화! 반드시 수동으로 켜야 함 │
│ │
│ 운영 환경에서 이 설정 없으면: │
│ 노드 페일오버 후 → stale topology → READONLY 에러 폭주 │
│ │
└─────────────────────────────────────────────────────────────────┘
Master/Replica 읽기 분산 (ReadFrom)
┌─────────────────────────────────────────────────────────────────┐
│ ReadFrom 전략 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────┬────────────────────────────────────┐ │
│ │ 전략 │ 동작 │ │
│ ├────────────────────┼────────────────────────────────────┤ │
│ │ MASTER │ 마스터에서만 읽기 (기본값) │ │
│ │ MASTER_PREFERRED │ 마스터 우선, 불가 시 레플리카 │ │
│ │ REPLICA │ 레플리카에서만 읽기 │ │
│ │ REPLICA_PREFERRED │ 레플리카 우선, 불가 시 마스터 │ │
│ │ LOWEST_LATENCY │ 지연시간 가장 낮은 노드 │ │
│ │ ANY │ 아무 노드 │ │
│ └────────────────────┴────────────────────────────────────┘ │
│ │
│ ⚠️ REPLICA 계열 사용 시 복제 지연(replication lag)으로 │
│ 오래된 데이터를 읽을 수 있음 │
│ → Eventually Consistent 읽기를 허용하는 경우에만 사용 │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 운영 주의사항과 트러블슈팅
가장 위험한 문제: 메모리 누수
┌─────────────────────────────────────────────────────────────────┐
│ Lettuce 메모리 누수 패턴 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 문제 1: 언바운드 커맨드 큐 (가장 위험!) │
│ │
│ Redis 연결 끊김 → 자동 재연결 시도 │
│ 그 사이 발행된 명령 → 무제한 큐에 버퍼링 │
│ 재연결이 오래 걸리면 → 힙 메모리 폭증 → OOM │
│ │
│ 기본값: requestQueueSize = Integer.MAX_VALUE (사실상 무제한) │
│ │
│ 해결: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ClientOptions.builder() │ │
│ │ .requestQueueSize(1000) // 큐 크기 제한! │ │
│ │ .disconnectedBehavior( │ │
│ │ DisconnectedBehavior.REJECT_COMMANDS) │ │
│ │ // 끊김 시 즉시 에러 반환 │ │
│ │ .autoReconnect(true) │ │
│ │ .build(); │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 2: Dead Connection 미감지 (최대 15분 대기) │
│ │
│ 하드웨어 장애 시 TCP RST 없이 연결 유실 │
│ OS 기본 TCP KeepAlive: 7200초 (2시간) 후에야 감지 │
│ 재시도 포함 시 최대 925초(~15분) 대기 │
│ │
│ 해결: TCP KeepAlive + epoll 설정 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ KeepAliveOptions.builder() │ │
│ │ .idle(Duration.ofSeconds(5)) // 5초 유휴 후 │ │
│ │ .interval(Duration.ofSeconds(5)) // 5초 간격 │ │
│ │ .count(3) // 3번 실패 시 │ │
│ │ .enable().build(); │ │
│ │ │ │
│ │ // 총 감지 시간: 5 + 5×3 = 20초 │ │
│ │ // (기본 2시간에서 20초로 단축!) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 문제 3: EventLoop 블로킹 → 전체 시스템 멈춤 │
│ │
│ // 절대 하지 말 것! │
│ reactive.get("key").subscribe(value -> { │
│ syncCommands.get("other"); // EventLoop 블로킹 → 데드락 │
│ }); │
│ │
│ // 올바른 방법 │
│ reactive.get("key") │
│ .flatMap(v -> reactive.get("other")) // 비동기 체이닝 │
│ .subscribe(System.out::println); │
│ │
└─────────────────────────────────────────────────────────────────┘
커넥션 풀이 필요한 경우 vs 불필요한 경우
┌─────────────────────────────────────────────────────────────────┐
│ Lettuce 커넥션 풀링 판단 기준 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────┬──────────┬───────────────────┐ │
│ │ 시나리오 │ 풀 필요? │ 이유 │ │
│ ├──────────────────────────┼──────────┼───────────────────┤ │
│ │ 일반 GET/SET (멀티스레드)│ 불필요 │ 단일 커넥션 공유 │ │
│ │ MULTI/EXEC 트랜잭션 │ 필요 │ 커넥션 상태 변경 │ │
│ │ BLPOP/BRPOP 블로킹 │ 필요 │ 커넥션 점유 │ │
│ │ WATCH 명령 │ 필요 │ 상태 바인딩 │ │
│ │ 리액티브/비동기 워크로드│ 불필요 │ 기본 설계 활용 │ │
│ │ 파이프라인 집중 워크로드│ 권장 │ 처리량 향상 │ │
│ └──────────────────────────┴──────────┴───────────────────┘ │
│ │
│ 경험 법칙: │
│ "커넥션 상태를 변경하는 명령을 쓰면 풀이 필요하다" │
│ "상태를 변경하지 않으면 단일 커넥션으로 충분하다" │
│ │
└─────────────────────────────────────────────────────────────────┘
클러스터 환경 함정
┌─────────────────────────────────────────────────────────────────┐
│ 클러스터 운영 시 주의사항 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┬─────────────────────────────────────────┐ │
│ │ 문제 │ 해결책 │ │
│ ├────────────────┼─────────────────────────────────────────┤ │
│ │ READONLY 에러 │ Adaptive topology refresh 활성화 │ │
│ │ (페일오버 후) │ enableAllAdaptiveRefreshTriggers() │ │
│ ├────────────────┼─────────────────────────────────────────┤ │
│ │ DNS 캐싱 문제 │ networkaddress.cache.ttl=0 │ │
│ │ (옛 IP 사용) │ JVM 시스템 프로퍼티 설정 │ │
│ ├────────────────┼─────────────────────────────────────────┤ │
│ │ 비멱등 명령 │ Command Replay Filter 사용 (6.6+) │ │
│ │ 중복 실행 │ INCR/DECR 재시도 시 값 2배 증가 │ │
│ ├────────────────┼─────────────────────────────────────────┤ │
│ │ iptables 차단 │ TCP KeepAlive 설정 필수 │ │
│ │ 후 감지 불가 │ RST 없이 연결 유실 시 │ │
│ └────────────────┴─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 최신 동향 (2024-2026)
Lettuce 6.x~7.x 주요 변경
┌─────────────────────────────────────────────────────────────────┐
│ Lettuce 버전별 핵심 변경사항 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┬─────────────────────────────────────────────────┐ │
│ │ 버전 │ 핵심 변경 │ │
│ ├──────┼─────────────────────────────────────────────────┤ │
│ │ 6.0 │ RESP3 지원, Kotlin Coroutine, RxJava3 │ │
│ │ 6.1 │ Micrometer 지원, io_uring, KeepAlive 옵션 │ │
│ │ 6.2 │ RedisCredentialsProvider (동적 인증) │ │
│ │ 6.3 │ Redis Functions (FCALL), Micrometer Tracing │ │
│ │ 6.4 │ Hash field expiration, Sharded Pub/Sub │ │
│ │ 6.5 │ RedisJSON 지원 │ │
│ │ 6.6 │ HGETDEL/HGETEX, Command Replay Filter │ │
│ │ 6.7 │ Vector Sets (Redis 8.0), epoll 기본 │ │
│ │ 6.8 │ RediSearch 지원 │ │
│ │ 7.0 │ Redis 2.6~8.4 지원, Java 8+/21 호환 │ │
│ └──────┴─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Virtual Thread (Java 21) 지원
┌─────────────────────────────────────────────────────────────────┐
│ Virtual Thread와 Lettuce │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Lettuce: 기술적으로 호환되지만 주의 필요 │
│ │
│ ├── Lettuce의 I/O는 이미 Netty 기반 논블로킹 │
│ │ → 가상 스레드의 이점이 제한적 │
│ │ │
│ ├── 알려진 이슈: │
│ │ ├── 커넥션 풀 + 가상 스레드 → Pool exhausted 버그 │
│ │ │ (Issue #2867, 2024년) │
│ │ └── Netty의 synchronized 블록 → 가상 스레드 핀닝 │
│ │ │
│ └── 권장: 가상 스레드 환경에서도 async/reactive API 사용 │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ Jedis + Virtual Thread: │
│ JedisPool.getResource()가 영구 블로킹되는 버그 존재 │
│ (Apache Commons Pool 이슈) │
│ → Virtual Thread 환경에서는 Lettuce가 더 안전 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 대안 클라이언트: Redisson
┌─────────────────────────────────────────────────────────────────┐
│ Redisson: 분산 객체 플랫폼 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Jedis/Lettuce = "Redis 클라이언트" │
│ Redisson = "Redis 위의 분산 자바 객체 플랫폼" │
│ │
│ ┌────────────────┬──────────┬──────────┬──────────┐ │
│ │ 기능 │ Jedis │ Lettuce │ Redisson │ │
│ ├────────────────┼──────────┼──────────┼──────────┤ │
│ │ 분산 락 │ 수동구현 │ 수동구현 │ RLock │ │
│ │ 분산 컬렉션 │ 저수준 │ 저수준 │ RMap 등 │ │
│ │ Near Cache │ 없음 │ 없음 │ 45x 향상 │ │
│ │ JCache API │ 없음 │ 없음 │ 지원 │ │
│ │ 직렬화 코덱 │ 없음 │ 제한적 │ Kryo 등 │ │
│ │ 분산 스케줄러 │ 없음 │ 없음 │ 지원 │ │
│ │ Reactive │ 없음 │ 지원 │ 지원 │ │
│ │ Valkey 지원 │ 제한적 │ 호환 │ 완전 │ │
│ └────────────────┴──────────┴──────────┴──────────┘ │
│ │
│ Redisson 분산 락 (RLock): │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ RLock lock = redisson.getLock("myLock"); │ │
│ │ lock.lock(); // 30초 watchdog 자동 갱신 │ │
│ │ try { │ │
│ │ // 임계 구역 │ │
│ │ } finally { │ │
│ │ lock.unlock(); │ │
│ │ } │ │
│ │ │ │
│ │ 내부 동작: │ │
│ │ 1. Lua 스크립트로 원자적 잠금 (SET NX PX) │ │
│ │ 2. Watchdog: 인스턴스 살아있는 동안 만료 자동 연장 │ │
│ │ 3. pub/sub 채널로 잠금 해제 알림 → busy-wait 없음 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10. 클라이언트 선택 가이드
┌─────────────────────────────────────────────────────────────────┐
│ 최종 선택 가이드 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Q1: 분산 락, 분산 컬렉션이 필요한가? │
│ │ │
│ ├── YES → Redisson │
│ │ │
│ └── NO → Q2: 비동기/리액티브가 필요한가? │
│ │ │
│ ├── YES → Lettuce (유일한 선택) │
│ │ │
│ └── NO → Q3: Redis Cluster를 쓰는가? │
│ │ │
│ ├── YES → Lettuce (Cluster 비동기 지원) │
│ │ │
│ └── NO → Q4: 팀 경험과 프로젝트 규모? │
│ │ │
│ ├── 단순/레거시 → Jedis (학습 곡선 낮음) │
│ └── 새 프로젝트 → Lettuce (Spring Boot 기본값) │
│ │
│ ────────────────────────────────────────────────── │
│ │
│ 요약: │
│ │
│ ┌────────────────────────┬──────────────────────┐ │
│ │ 상황 │ 추천 │ │
│ ├────────────────────────┼──────────────────────┤ │
│ │ Spring Boot 새 프로젝트│ Lettuce (기본값 유지) │ │
│ │ Spring WebFlux │ Lettuce │ │
│ │ 고동시성 비동기 처리 │ Lettuce │ │
│ │ Redis Cluster + 비동기 │ Lettuce │ │
│ │ 분산 락/컬렉션 필요 │ Redisson │ │
│ │ 단순 동기 API만 필요 │ Jedis (레거시 호환) │ │
│ │ Java 21 Virtual Thread│ Lettuce │ │
│ │ Valkey 완전 지원 │ Redisson │ │
│ └────────────────────────┴──────────────────────┘ │
│ │
│ 대부분의 경우: Lettuce를 기본으로 사용하고, │
│ 분산 락이 필요하면 Redisson을 추가하는 것이 최선이다. │
│ │
└─────────────────────────────────────────────────────────────────┘
Spring Boot에서 Jedis로 전환하는 방법
<!-- Maven: Lettuce 제거 + Jedis 추가 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
// Gradle
implementation('org.springframework.boot:spring-boot-starter-data-redis') {
exclude group: 'io.lettuce', module: 'lettuce-core'
}
implementation 'redis.clients:jedis'
# application.properties (Jedis 풀 설정)
spring.data.redis.jedis.pool.max-active=50
spring.data.redis.jedis.pool.max-idle=10
spring.data.redis.jedis.pool.min-idle=2
spring.data.redis.jedis.pool.max-wait=2000ms
11. 참고자료
[공식 문서]
1. Lettuce Reference Guide
https://redis.github.io/lettuce/
2. GitHub - redis/lettuce
https://github.com/redis/lettuce
3. Spring Data Redis - Drivers
https://docs.spring.io/spring-data/redis/reference/redis/drivers.html
4. Redis Production Usage Guide for Lettuce
https://redis.io/docs/latest/develop/clients/lettuce/produsage/
[블로그 & 비교]
5. Redis Blog - Jedis vs. Lettuce: An Exploration
https://redis.io/blog/jedis-vs-lettuce-an-exploration/
6. Redis Blog - Lettuce Joins Redis' Official Client Family
https://redis.io/blog/lettuce-joins-redis-official-client-family/
7. Spring Boot Issue #10480 - Consider Lettuce instead of Jedis
https://github.com/spring-projects/spring-boot/issues/10480
8. Baeldung - Introduction to Lettuce
https://www.baeldung.com/java-redis-lettuce
[고급 설정]
9. Lettuce Wiki - Connection Pooling
https://github.com/redis/lettuce/wiki/Connection-Pooling
10. Lettuce Wiki - ReadFrom Settings
https://github.com/lettuce-io/lettuce-core/wiki/ReadFrom-Settings
11. Lettuce Wiki - Pipelining and Command Flushing
https://github.com/redis/lettuce/wiki/Pipelining-and-command-flushing
[대안 클라이언트]
12. Redisson - Feature Comparison vs Lettuce
https://redisson.pro/blog/feature-comparison-redisson-vs-lettuce.html
13. Redisson - Distributed Locks Wiki
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers