개발/java

[redis] 자바에서 사용 가능한 레디스 클라이언트

방푸린 2025. 1. 23. 16:14
반응형

환경: springframework 4.2, java8

 

자바와 레디스를 연결하기 위한 레디스 클라이언트는 크게 3개가 있다.

1. Jedis

특징

  • 초기 Redis Java 클라이언트 중 하나로, 간단하고 직관적인 API 제공.
  • Redis 명령어 대부분을 지원하며, 설정이 간단함.
  • 싱글스레드 기반으로 설계됨.
  • Jedis는 Redis 클라이언트를 위한 핵심적인 기능만 포함하며, 추가적인 추상화나 고급 기능(분산 락, 세마포어 등)은 제공하지 않음
  • 다른 클라이언트(예: Redisson)에 비해 의존성이 적고, 실행 크기가 작음, 가벼움

장점

  • 사용법이 단순하고 직관적이어서 빠르게 배울 수 있음.
  • Redis와의 네이티브 한 연동 및 모든 명령어를 제공.
  • 간단한 애플리케이션에서 적합.

단점

  • Thread-Safe 하지 않음: 멀티스레드 환경에서 사용하려면 JedisPool을 사용해야 함.
  • 싱글스레드 기반이라 멀티스레드 환경에서는 성능이 저하될 수 있음.

추천 사용 시나리오

  • 단일 스레드 기반 애플리케이션.
  • Redis를 단순 캐싱 또는 데이터 저장소로 사용하는 경우.

2. Lettuce

특징

  • 비동기 및 동기 API를 모두 제공하며, Reactive Streams(Flux/Mono)도 지원.
  • 기본적으로 비동기적으로 실행되고, 결과는 CompletionStage 또는 Future를 통해 반환됨
  • Netty를 기반으로 한 non blocking I/O 모델 사용.
  • Thread-Safe: 단일 커넥션을 여러 스레드에서 공유 가능.

장점

  • 멀티스레드 환경에서 효율적이며 Thread-Safe.
  • 비동기 작업에 유리하며 고성능 제공.
  • Reactive 프로그래밍 환경과의 호환성이 뛰어남.
  • 클러스터와 Sentinel 환경 지원.

단점

  • API가 Jedis보다 다소 복잡할 수 있음.
  • 초심자에게는 학습 곡선이 약간 높음.

추천 사용 시나리오

  • 비동기 작업이 많은 고성능 애플리케이션.
  • 멀티스레드 환경.
  • 클러스터 또는 Sentinel 기반 Redis 설정.
  • Reactive 프로그래밍(Spring WebFlux 등)을 사용하는 경우.

3. Redisson

특징

  • Redis를 기반으로 한 고급 분산 기능 제공(분산 락, 분산 캐시 등).
  • Redis를 Java의 분산 데이터 구조와 유사하게 다룰 수 있는 API 제공.
  • Thread-Safe 하며, 다양한 고급 기능이 포함됨.

장점

  • 분산 락, 분산 세마포어, RMap, RList 등 고급 데이터 구조 지원.
  • 클러스터, Sentinel, 레플리카 환경에서 유연하게 동작.
  • Redis 클라이언트를 단순한 캐싱 도구 이상으로 활용 가능.

단점

  • 다른 클라이언트보다 무겁고 약간의 추가 오버헤드 발생.
  • Jedis나 Lettuce에 비해 더 많은 메모리 사용 가능.

추천 사용 시나리오

  • 분산 락, 세마포어, 큐와 같은 고급 분산 기능이 필요한 환경.
  • Redis를 데이터베이스 이상의 목적으로 사용하는 경우.
  • 복잡한 클러스터 환경.

 

jedis 사용

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.8.4.RELEASE</version>
</dependency>

<!-- Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
 public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    log.debug("redisKey: {}, ttl: {}", redisKey, lockSecond);
    try {
      acquireLock(redisKey, lockSecond);
      return logic.get();
    } finally {
      releaseLock(redisKey);
    }
  }

  private void acquireLock(String redisKey, long lockSecond) {
    final RedisCallback<Boolean> redisCallback = connection -> {
      byte[] key = redisTemplate.getStringSerializer().serialize(redisKey);
      byte[] value = redisTemplate.getStringSerializer().serialize("locked");
      return connection.setNX(key, value) && connection.expire(key, lockSecond);
    };

    boolean success = redisTemplate.execute(redisCallback);
    if (!success) {
      throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
    }
  }

  private void releaseLock(String redisKey) {
    redisTemplate.delete(redisKey);
  }

위처럼 분산락을 jedis를 사용하여 개발하면 락을 얻고 ttl을 주고 해제하는 과정을 수동으로 개발해야 한다.

redisson으로 수정

redisson을 사용하면 아래와 같이 라이브러리에서 제공하는 기능을 사용하여 간소화하게 짤 수 있다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    RLock lock = redissonClient.getLock(redisKey); // Redis 분산 락 생성
    log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

    try {
        // 락을 시도하고 성공할 경우 논리 실행
        if (lock.tryLock(0, lockSecond, TimeUnit.SECONDS)) { 
            return logic.get();
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new ApiLockException("락을 획득하는 동안 인터럽트가 발생했습니다. key: " + redisKey, e);
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock(); // 락 해제
            log.debug("Lock released for redisKey: {}", redisKey);
        }
    }
}

lettuce...?

호옥시 lettuce로 바꾸면 어떻게 될까 궁금해서 chat gpt에게 물어봤다.

public <T> T lockProcess(String redisKey, long lockSecond, Supplier<T> logic) {
    try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
        RedisCommands<String, String> commands = connection.sync();
        log.debug("Acquiring lock for redisKey: {}, ttl: {} seconds", redisKey, lockSecond);

        if (acquireLock(commands, redisKey, lockSecond)) {
            try {
                return logic.get();
            } finally {
                releaseLock(commands, redisKey);
            }
        } else {
            throw new ApiLockException("이미 처리 중 입니다. key: " + redisKey);
        }
    }
}

private boolean acquireLock(RedisCommands<String, String> commands, String redisKey, long lockSecond) {
    String result = commands.set(redisKey, "locked", io.lettuce.core.SetArgs.Builder.nx().ex(lockSecond));
    return "OK".equals(result); // Redis SET NX PX 결과 확인
}

private void releaseLock(RedisCommands<String, String> commands, String redisKey) {
    commands.del(redisKey); // 락 해제
    log.debug("Lock released for redisKey: {}", redisKey);
}

lettuce는 동기 동작을 위해 .sync()를 꼭 넣어야 하고(비동기는 .async()) 나머지는 jedis와 비슷하게 직접(setnx) 락을 설정하고 ttl도 설정해야 한다.

그리고 lettuce의 StatefulRedisConnection를 사용하는 경우 close 해줘야 한다. AutoCloseable를 implement 했기 때문에 try-with-resources 사용 가능하다.

 

  • 연결 풀을 사용할 경우에는, 연결이 자동으로 풀로 반환되며 그 자체로 명시적인 종료가 필요하지 않지만, StatefulRedisConnection은 연결 풀을 사용하지 않고, 각 연결을 명시적으로 관리하는 방식.
  • 연결 풀을 사용하면 연결을 가져오고 반환하는 방식으로 관리되기 때문에, 풀에서 자동으로 연결을 회수하고 재사용할 수. 하지만 StatefulRedisConnection은 풀을 사용하지 않기 때문에 사용자가 직접 연결을 닫고 관리해야 합니다.

 

shutdown을 수동으로 해야하나..?

참고로 레디스 연결 객체인 RedisClient와 RedisConnectionFactory는 프로그램 종료 시 shutdown이 되어야 하는데, 빈으로 등록되어 있는 경우 Spring Boot에서는 자동으로 연결 종료가 처리된다. 이렇게 되면 명시적인 shutdown() 호출은 필요하지 않는다. Spring Boot는 빈 관리 및 라이프사이클을 자동으로 처리하기 때문에, 애플리케이션 종료 시 리소스를 자동으로 정리한다.

하지만 Spring Framework에서는 명시적인 shutdown() 호출이 필요할 수 있다. 왜냐하면 Spring Framework에서는 자동으로 리소스를 관리하는 기능이 내장되어 있지 않기 때문...?

  • hmmmmmm?? 진짠지 모르겠음.. 레거시에서도 수동으로 shutdown 한건 본적이 없음

 


분산락을 위해 보통 @AOP로 만들 수도 있지만 위와 같은 고차함수 방식을 더 선호한다.

아래와 같은 단점이 있기에..

  • public에만 사용가능
  • 값 넘길 때 함수 argument 첫번째 값 주의 필요
  • @Around( "@within(com.annotation.. <-- 와 같이 문자열로 관리하는 것에 대한 부담
  • 락의 범위가 넓어질 수 있음

 

고차함수 분산락을 사용할 때는 아래와 같이 하면 된다.

final String redisKey = redisKey("invenChangeExpire", String.valueOf(sno));

return this.apiLockService.lockProcess(redisKey, () -> {
	... 로직
    });
728x90
반응형