Spring Boot에서 Redis를 쓸 때, 내부적으로 어떤 클라이언트가 요청을 보내고 있는지 생각해 본 적 있나요?

spring-boot-starter-data-redis를 추가하면 RedisTemplate이 알아서 동작합니다. 그런데 그 아래에서 실제로 TCP 커넥션을 열고 RESP 프로토콜로 명령을 주고받는 건 Redis 클라이언트 라이브러리 입니다. Java 생태계에서는 Lettuce, Jedis, Redisson 세 가지가 대표적이고, 각각의 아키텍처와 철학이 상당히 다릅니다.

개념 정의

Redis 클라이언트 는 애플리케이션과 Redis 서버 사이에서 커넥션 관리, 명령 직렬화(RESP), 응답 파싱을 담당하는 라이브러리입니다. 어떤 클라이언트를 선택하느냐에 따라 커넥션 모델, 스레드 안전성, 지원하는 API 스타일이 완전히 달라집니다.

세 클라이언트 한눈에 비교

항목LettuceJedisRedisson
I/O 모델Netty 비동기/논블로킹동기 블로킹Netty 비동기/논블로킹
** 스레드 안전성**스레드 안전 (커넥션 공유)스레드 안전하지 않음 (풀 필요)스레드 안전
** 커넥션 모델**기본 단일 커넥션 공유요청마다 풀에서 커넥션 할당커넥션 풀 내장
API 스타일동기/비동기/리액티브동기 전용동기/비동기/리액티브
Spring 통합기본 클라이언트 (2.0+)별도 설정 필요spring-boot-starter 제공
** 주요 강점**경량, 높은 처리량단순함, 쉬운 디버깅분산 자료구조
** 적합한 상황**범용 캐시/세션간단한 동기 작업분산 락/큐/맵 필요 시

Lettuce — Netty 기반 비동기 클라이언트

아키텍처

Lettuce는 Netty의 이벤트 루프 위에서 동작합니다. 가장 큰 특징은 ** 하나의 TCP 커넥션을 여러 스레드가 공유 **한다는 점입니다.

PLAINTEXT
┌──────────┐     ┌──────────┐     ┌──────────┐
│ 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를 사용할 때 가장 자연스러운 선택입니다.

JAVA
// 리액티브 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가 기본 클라이언트입니다. 별도 설정 없이 바로 사용할 수 있습니다.

JAVA
@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.ymlspring.data.redis.hostport만 설정하면 됩니다. 커넥션 풀 설정은 트랜잭션이나 블로킹 명령을 직접 사용할 때만 추가하면 충분합니다.

Jedis — 동기 블로킹 클라이언트

아키텍처

Jedis는 가장 오래된 Java Redis 클라이언트입니다. 설계 철학이 단순합니다 — ** 하나의 요청, 하나의 커넥션, 블로킹 I/O**.

PLAINTEXT
┌──────────┐     ┌──────────┐     ┌──────────┐
│ Thread-1 │     │ Thread-2 │     │ Thread-3 │
└────┬─────┘     └────┬─────┘     └────┬─────┘
     │                │                │
┌────▼────┐     ┌────▼────┐     ┌────▼────┐
│ Conn-1  │     │ Conn-2  │     │ Conn-3  │
└────┬────┘     └────┬────┘     └────┬────┘
     │                │                │
     └───────┬────────┴────────┬───────┘
             │   JedisPool     │
      ┌──────▼─────────────────▼──────┐
      │          Redis Server          │
      └────────────────────────────────┘

Jedis 인스턴스 하나는 스레드 안전하지 않습니다. 멀티스레드 환경에서는 반드시 JedisPool을 사용해야 합니다.

JedisPool 설정

JAVA
@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);
    }
}
JAVA
// 사용 패턴 — try-with-resources로 반드시 반환
public String getValue(JedisPool pool, String key) {
    try (Jedis jedis = pool.getResource()) {
        return jedis.get(key);
    }
    // try 블록을 벗어나면 자동으로 풀에 반환
}

Jedis를 선택하는 경우

  • ** 디버깅이 쉬워야 할 때 **: 동기 블로킹이라 콜스택이 직관적입니다
  • ** 비동기가 필요 없는 배치 작업 **: 단순한 캐시 조회/저장
  • ** 팀이 동기 모델에 익숙할 때 **: 러닝 커브가 낮습니다

다만 Spring Boot 환경에서 Jedis를 쓰려면 별도로 의존성을 교체해야 합니다.

YAML
# 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 분산 객체 를 구현합니다.

PLAINTEXT
┌─────────────────────────────────────────┐
│           Application Code              │
│  RMap, RLock, RQueue, RSemaphore ...    │
├─────────────────────────────────────────┤
│              Redisson                   │
│    (분산 객체 → Redis 명령 변환)          │
├─────────────────────────────────────────┤
│         Netty (비동기 I/O)               │
├─────────────────────────────────────────┤
│            Redis Server                 │
└─────────────────────────────────────────┘

주요 분산 자료구조

자료구조설명Java 대응
RMap분산 HashMapConcurrentHashMap
RLock분산 재진입 락ReentrantLock
RReadWriteLock분산 읽기/쓰기 락ReadWriteLock
RQueue / RBlockingQueue분산 큐BlockingQueue
RSemaphore분산 세마포어Semaphore
RCountDownLatch분산 카운트다운 래치CountDownLatch
RBloomFilter분산 블룸 필터
RAtomicLong분산 원자 카운터AtomicLong

RLock 사용 예제

JAVA
@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 설정

JAVA
@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 기반)
  • ** 분산 락 **: RedissonClientRLock

두 라이브러리가 각각 별도의 커넥션을 관리하므로, 같은 Redis 서버에 대해 문제없이 공존할 수 있습니다.

JAVA
@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을 추가하는 게 가장 현실적인 접근입니다.

댓글 로딩 중...