Redis 클라이언트 비교 — Lettuce, Jedis, Redisson의 선택 기준
Spring Boot에서 Redis를 쓸 때, 내부적으로 어떤 클라이언트가 요청을 보내고 있는지 생각해 본 적 있나요?
spring-boot-starter-data-redis를 추가하면 RedisTemplate이 알아서 동작합니다. 그런데 그 아래에서 실제로 TCP 커넥션을 열고 RESP 프로토콜로 명령을 주고받는 건 Redis 클라이언트 라이브러리 입니다. Java 생태계에서는 Lettuce, Jedis, Redisson 세 가지가 대표적이고, 각각의 아키텍처와 철학이 상당히 다릅니다.
개념 정의
Redis 클라이언트 는 애플리케이션과 Redis 서버 사이에서 커넥션 관리, 명령 직렬화(RESP), 응답 파싱을 담당하는 라이브러리입니다. 어떤 클라이언트를 선택하느냐에 따라 커넥션 모델, 스레드 안전성, 지원하는 API 스타일이 완전히 달라집니다.
세 클라이언트 한눈에 비교
| 항목 | Lettuce | Jedis | Redisson |
|---|---|---|---|
| I/O 모델 | Netty 비동기/논블로킹 | 동기 블로킹 | Netty 비동기/논블로킹 |
| ** 스레드 안전성** | 스레드 안전 (커넥션 공유) | 스레드 안전하지 않음 (풀 필요) | 스레드 안전 |
| ** 커넥션 모델** | 기본 단일 커넥션 공유 | 요청마다 풀에서 커넥션 할당 | 커넥션 풀 내장 |
| API 스타일 | 동기/비동기/리액티브 | 동기 전용 | 동기/비동기/리액티브 |
| Spring 통합 | 기본 클라이언트 (2.0+) | 별도 설정 필요 | spring-boot-starter 제공 |
| ** 주요 강점** | 경량, 높은 처리량 | 단순함, 쉬운 디버깅 | 분산 자료구조 |
| ** 적합한 상황** | 범용 캐시/세션 | 간단한 동기 작업 | 분산 락/큐/맵 필요 시 |
Lettuce — Netty 기반 비동기 클라이언트
아키텍처
Lettuce는 Netty의 이벤트 루프 위에서 동작합니다. 가장 큰 특징은 ** 하나의 TCP 커넥션을 여러 스레드가 공유 **한다는 점입니다.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Thread-1 │ │ Thread-2 │ │ Thread-3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────┬────────┴────────┬───────┘
│ │
┌──────▼─────────────────▼──────┐
│ Lettuce (Netty Event Loop) │
│ ── 단일 TCP 커넥션 ── │
└──────────────┬────────────────┘
│
┌──────▼──────┐
│ Redis Server │
└─────────────┘
Redis 자체가 싱글 스레드로 명령을 순차 처리하기 때문에, 클라이언트 쪽에서도 하나의 커넥션으로 명령을 파이프라이닝하면 충분히 높은 처리량을 낼 수 있습니다. 커넥션 풀 없이도 수천 TPS를 처리하는 이유가 여기 있습니다.
커넥션 공유가 안 되는 경우
단일 커넥션 공유가 만능은 아닙니다. 다음 상황에서는 ** 전용 커넥션(dedicated connection)** 이 필요합니다.
- **MULTI/EXEC 트랜잭션 **: 커넥션을 공유하면 다른 스레드의 명령이 MULTI~EXEC 사이에 끼어듭니다
- **BLPOP, BRPOP 등 블로킹 명령 **: 커넥션이 블로킹되면 다른 스레드도 멈춥니다
- SUBSCRIBE (Pub/Sub): 구독 모드에 진입하면 일반 명령을 보낼 수 없습니다
공부하다 보니, Lettuce의 커넥션 공유 모델은 "대부분의 상황에서 효율적"이지만 트랜잭션이나 블로킹 명령을 쓸 때는 반드시 전용 커넥션을 써야 한다는 점이 중요했습니다.
리액티브 지원
Lettuce는 Project Reactor의 Mono/Flux를 네이티브로 지원합니다. WebFlux 환경에서 Redis를 사용할 때 가장 자연스러운 선택입니다.
// 리액티브 API — Mono<String> 반환
Mono<String> value = reactiveCommands.get("user:1001:name");
value.subscribe(name -> System.out.println("이름: " + name));
Spring Boot에서 Lettuce 설정
Spring Boot 2.0부터 Lettuce가 기본 클라이언트입니다. 별도 설정 없이 바로 사용할 수 있습니다.
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config =
new RedisStandaloneConfiguration("localhost", 6379);
// 커넥션 풀이 필요한 경우 (트랜잭션, 블로킹 명령 사용 시)
GenericObjectPoolConfig<?> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(10); // 최대 커넥션 수
poolConfig.setMaxIdle(5); // 최대 유휴 커넥션
poolConfig.setMinIdle(2); // 최소 유휴 커넥션
LettucePoolingClientConfiguration clientConfig =
LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(Duration.ofSeconds(3)) // 명령 타임아웃
.build();
return new LettuceConnectionFactory(config, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
대부분의 경우
application.yml에spring.data.redis.host와port만 설정하면 됩니다. 커넥션 풀 설정은 트랜잭션이나 블로킹 명령을 직접 사용할 때만 추가하면 충분합니다.
Jedis — 동기 블로킹 클라이언트
아키텍처
Jedis는 가장 오래된 Java Redis 클라이언트입니다. 설계 철학이 단순합니다 — ** 하나의 요청, 하나의 커넥션, 블로킹 I/O**.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Thread-1 │ │ Thread-2 │ │ Thread-3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Conn-1 │ │ Conn-2 │ │ Conn-3 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────┬────────┴────────┬───────┘
│ JedisPool │
┌──────▼─────────────────▼──────┐
│ Redis Server │
└────────────────────────────────┘
Jedis 인스턴스 하나는 스레드 안전하지 않습니다. 멀티스레드 환경에서는 반드시 JedisPool을 사용해야 합니다.
JedisPool 설정
@Configuration
public class JedisConfig {
@Bean
public JedisPool jedisPool() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(20); // 최대 커넥션 수
poolConfig.setMaxIdle(10); // 최대 유휴 커넥션
poolConfig.setMinIdle(5); // 최소 유휴 커넥션
poolConfig.setMaxWaitMillis(3000); // 풀에서 커넥션 대기 최대 시간
// 유효하지 않은 커넥션 반환 방지
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(true);
return new JedisPool(poolConfig, "localhost", 6379, 2000);
}
}
// 사용 패턴 — try-with-resources로 반드시 반환
public String getValue(JedisPool pool, String key) {
try (Jedis jedis = pool.getResource()) {
return jedis.get(key);
}
// try 블록을 벗어나면 자동으로 풀에 반환
}
Jedis를 선택하는 경우
- ** 디버깅이 쉬워야 할 때 **: 동기 블로킹이라 콜스택이 직관적입니다
- ** 비동기가 필요 없는 배치 작업 **: 단순한 캐시 조회/저장
- ** 팀이 동기 모델에 익숙할 때 **: 러닝 커브가 낮습니다
다만 Spring Boot 환경에서 Jedis를 쓰려면 별도로 의존성을 교체해야 합니다.
# build.gradle
dependencies {
implementation('org.springframework.boot:spring-boot-starter-data-redis') {
exclude group: 'io.lettuce', module: 'lettuce-core'
}
implementation 'redis.clients:jedis'
}
Redisson — 분산 자료구조 클라이언트
아키텍처
Redisson은 Lettuce, Jedis와 근본적으로 다른 레벨에서 동작합니다. 단순히 Redis 명령을 보내는 것이 아니라, Redis 위에 Java 분산 객체 를 구현합니다.
┌─────────────────────────────────────────┐
│ Application Code │
│ RMap, RLock, RQueue, RSemaphore ... │
├─────────────────────────────────────────┤
│ Redisson │
│ (분산 객체 → Redis 명령 변환) │
├─────────────────────────────────────────┤
│ Netty (비동기 I/O) │
├─────────────────────────────────────────┤
│ Redis Server │
└─────────────────────────────────────────┘
주요 분산 자료구조
| 자료구조 | 설명 | Java 대응 |
|---|---|---|
RMap | 분산 HashMap | ConcurrentHashMap |
RLock | 분산 재진입 락 | ReentrantLock |
RReadWriteLock | 분산 읽기/쓰기 락 | ReadWriteLock |
RQueue / RBlockingQueue | 분산 큐 | BlockingQueue |
RSemaphore | 분산 세마포어 | Semaphore |
RCountDownLatch | 분산 카운트다운 래치 | CountDownLatch |
RBloomFilter | 분산 블룸 필터 | — |
RAtomicLong | 분산 원자 카운터 | AtomicLong |
RLock 사용 예제
@Service
public class OrderService {
private final RedissonClient redissonClient;
public OrderService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
public void processOrder(Long orderId) {
// 주문 단위 분산 락 획득
RLock lock = redissonClient.getLock("lock:order:" + orderId);
try {
// 최대 5초 대기, 획득 후 10초 뒤 자동 해제
boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("락 획득 실패 — 다른 서버에서 처리 중");
}
// 임계 영역 — 재고 차감, 결제 처리 등
deductStock(orderId);
processPayment(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("락 대기 중 인터럽트 발생", e);
} finally {
// 반드시 해제 — 현재 스레드가 소유한 경우에만
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson의 RLock은 내부적으로 워치독(Watchdog) 을 사용합니다. leaseTime을 지정하지 않으면 30초 TTL로 락을 잡고, 워치독이 10초마다 TTL을 갱신합니다. 덕분에 작업이 오래 걸려도 락이 중간에 만료되는 문제를 방지할 수 있습니다.
직접
SETNX로 분산 락을 구현하면 TTL 연장, 재진입, 공정성 같은 문제를 모두 직접 처리해야 합니다. Redisson은 이런 부분을 추상화해서java.util.concurrent의 Lock처럼 쓸 수 있게 해줍니다.
Spring Boot + Redisson 설정
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setConnectionPoolSize(10)
.setConnectionMinimumIdleSize(5)
.setTimeout(3000);
return Redisson.create(config);
}
}
redisson-spring-boot-starter를 사용하면 application.yml만으로 설정할 수도 있습니다.
선택 가이드
선택은 생각보다 단순합니다.
기본값 → Lettuce
Spring Boot의 기본 클라이언트이고, 대부분의 캐시/세션 관리 시나리오에서 충분합니다. 특별한 이유가 없다면 Lettuce를 그대로 사용하면 됩니다.
분산 자료구조가 필요하면 → Redisson
분산 락, 분산 큐, 분산 맵 등 java.util.concurrent 수준의 분산 객체가 필요하면 Redisson이 답입니다. 특히 분산 락은 직접 구현하는 것보다 Redisson의 RLock을 사용하는 게 훨씬 안전합니다.
동기 블로킹이 편하면 → Jedis
비동기가 필요 없고, 간단한 Redis 작업만 하고, 디버깅 편의성이 중요하다면 Jedis도 괜찮은 선택입니다. 다만 Spring Boot에서 기본이 아니라는 점은 감안해야 합니다.
조합도 가능하다
실무에서는 Lettuce + Redisson 을 함께 쓰는 경우가 많습니다.
- **일반 캐시 조회/저장 **:
RedisTemplate(Lettuce 기반) - ** 분산 락 **:
RedissonClient의RLock
두 라이브러리가 각각 별도의 커넥션을 관리하므로, 같은 Redis 서버에 대해 문제없이 공존할 수 있습니다.
@Service
public class ProductService {
private final RedisTemplate<String, Object> redisTemplate; // Lettuce 기반
private final RedissonClient redissonClient; // Redisson
// 캐시 조회는 RedisTemplate
public Product getProduct(Long id) {
String key = "product:" + id;
Product cached = (Product) redisTemplate.opsForValue().get(key);
if (cached != null) return cached;
// 캐시 미스 시 DB 조회 + 캐시 저장
Product product = productRepository.findById(id).orElseThrow();
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
return product;
}
// 재고 차감은 분산 락으로 보호
public void deductStock(Long productId, int quantity) {
RLock lock = redissonClient.getLock("lock:stock:" + productId);
try {
lock.lock(10, TimeUnit.SECONDS);
// 재고 차감 로직
} finally {
lock.unlock();
}
}
}
정리
- Lettuce: Netty 기반 비동기 클라이언트, 단일 커넥션 공유로 효율적. Spring Boot 기본
- Jedis: 동기 블로킹 클라이언트, 단순하고 직관적. 멀티스레드 시 반드시 JedisPool 사용
- Redisson: Redis 위에 Java 분산 객체를 구현한 고수준 클라이언트. 분산 락/큐 필요 시 선택
- ** 실무 조합 **: Lettuce(일반 캐시) + Redisson(분산 락)이 가장 흔한 패턴
특별한 요구사항이 없다면 Spring Boot 기본인 Lettuce로 시작하고, 분산 락이나 분산 자료구조가 필요해지는 시점에 Redisson을 추가하는 게 가장 현실적인 접근입니다.