본문으로 건너뛰기
김신건의 로그

[Spring] Spring Data Redis

· 수정 · 📖 약 1분 · 331자/단어 #spring #redis #data #cache
spring redis, RedisTemplate, redis pub/sub, lettuce, redis cache spring

정의

Spring Data Redis는 Redis 클라이언트(Lettuce, Jedis)의 Spring 추상화. RedisTemplate, StringRedisTemplate (저수준), @Cacheable 통합, RedisRepository (객체 매핑), pub/sub 등.

설정

implementation("org.springframework.boot:spring-boot-starter-data-redis")

Lettuce 기본 (Netty, async, reactive). Jedis 대안.

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: ${REDIS_PASSWORD:}
      timeout: 5s
      lettuce:
        pool:
          max-active: 16
          max-idle: 8
          min-idle: 2
      cluster:
        nodes:
          - redis-1:6379
          - redis-2:6379
          - redis-3:6379
        max-redirects: 3
      sentinel:
        master: mymaster
        nodes:
          - sentinel-1:26379

StringRedisTemplate

가장 흔한 사용.

@Service
public class CacheService {
    private final StringRedisTemplate redis;

    public void set(String key, String value, Duration ttl) {
        redis.opsForValue().set(key, value, ttl);
    }

    public String get(String key) {
        return redis.opsForValue().get(key);
    }

    public Long increment(String key) {
        return redis.opsForValue().increment(key);
    }

    public boolean exists(String key) {
        return Boolean.TRUE.equals(redis.hasKey(key));
    }

    public void delete(String key) {
        redis.delete(key);
    }

    public Long expire(String key, Duration ttl) {
        return redis.getExpire(key, TimeUnit.SECONDS);
    }
}

RedisTemplate (객체)

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(cf);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }
}

@Service
public class UserCache {
    private final RedisTemplate<String, Object> redis;

    public void cache(User user) {
        redis.opsForValue().set("user:" + user.id(), user, Duration.ofHours(1));
    }

    public User get(Long id) {
        return (User) redis.opsForValue().get("user:" + id);
    }
}

다양한 데이터 구조

String

redis.opsForValue().set("key", "value");
redis.opsForValue().get("key");
redis.opsForValue().increment("counter");
redis.opsForValue().decrement("counter");
redis.opsForValue().setIfAbsent("key", "value", Duration.ofMinutes(5));    // SETNX + EX

Hash

HashOperations<String, String, String> hash = redis.opsForHash();
hash.put("user:1", "name", "Alice");
hash.put("user:1", "email", "alice@x.com");
hash.get("user:1", "name");
hash.entries("user:1");    // Map<String, String> 전체
hash.delete("user:1", "name");

List

ListOperations<String, String> list = redis.opsForList();
list.leftPush("queue", "item1");
list.rightPush("queue", "item2");
list.leftPop("queue");                  // BRPOP은 timeout
list.range("queue", 0, -1);              // 전체
list.size("queue");

Set

SetOperations<String, String> set = redis.opsForSet();
set.add("tags:post1", "java", "spring");
set.members("tags:post1");
set.isMember("tags:post1", "java");
set.intersect("tags:post1", "tags:post2");

Sorted Set (Leaderboard)

ZSetOperations<String, String> zset = redis.opsForZSet();
zset.add("leaderboard", "alice", 100);
zset.add("leaderboard", "bob", 85);
zset.incrementScore("leaderboard", "alice", 10);
zset.reverseRangeWithScores("leaderboard", 0, 9);    // top 10
zset.reverseRank("leaderboard", "alice");

Pub/Sub

@Configuration
public class PubSubConfig {
    @Bean
    public RedisMessageListenerContainer container(
        RedisConnectionFactory cf, MessageListenerAdapter listener
    ) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(cf);
        container.addMessageListener(listener, new PatternTopic("events:*"));
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(MyListener listener) {
        return new MessageListenerAdapter(listener, "onMessage");
    }
}

@Component
public class MyListener {
    public void onMessage(String message, String channel) {
        log.info("Received {} on {}", message, channel);
    }
}

// 발행
redis.convertAndSend("events:user-signup", "alice");

영속 메시지는 Stream 사용 (XADD/XREAD).

@Cacheable (Spring Cache + Redis)

spring:
  cache:
    type: redis
    redis:
      time-to-live: 600000
      cache-null-values: false
@Service
public class UserService {

    @Cacheable(value = "users", key = "#id")
    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

자세한 건 spring-cache 참고.

Redis Repository (객체 매핑)

@RedisHash("users")
public class User {
    @Id Long id;
    String email;
    @Indexed String name;    // 검색 인덱스 자동
    LocalDateTime createdAt;
}

public interface UserRepository extends CrudRepository<User, Long> {
    List<User> findByName(String name);    // 인덱스 활용
}

JPA 비슷한 API. 단 SQL 못 씀, 단순 lookup만.

Lock (분산)

@Component
public class DistributedLock {
    private final StringRedisTemplate redis;

    public boolean acquire(String key, String value, Duration ttl) {
        return Boolean.TRUE.equals(redis.opsForValue().setIfAbsent(key, value, ttl));
    }

    public boolean release(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Long result = redis.execute(
            new DefaultRedisScript<>(script, Long.class),
            List.of(key),
            value
        );
        return result != null && result > 0;
    }
}

또는 Redisson 라이브러리 (더 풍부).

자주 보는 패턴

Session 저장

spring:
  session:
    store-type: redis
    redis:
      namespace: myapp:session

Spring Session이 자동으로 Redis에 세션 저장. 여러 인스턴스 간 공유.

Rate Limit (token bucket)

public boolean tryAcquire(String key, int max, Duration window) {
    String script = """
        local count = redis.call('INCR', KEYS[1])
        if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end
        return count <= tonumber(ARGV[1])
    """;
    return Boolean.TRUE.equals(redis.execute(
        new DefaultRedisScript<>(script, Boolean.class),
        List.of(key),
        max, window.getSeconds()
    ));
}

Idempotency Key

public boolean processOnce(String key, Runnable action) {
    Boolean acquired = redis.opsForValue().setIfAbsent(
        "idempotent:" + key, "1", Duration.ofHours(24)
    );
    if (Boolean.TRUE.equals(acquired)) {
        action.run();
        return true;
    }
    return false;
}

같은 요청 중복 처리 방지.

Cache aside

public User getUser(Long id) {
    String key = "user:" + id;
    User cached = (User) redis.opsForValue().get(key);
    if (cached != null) return cached;

    User user = userRepository.findById(id).orElseThrow();
    redis.opsForValue().set(key, user, Duration.ofMinutes(10));
    return user;
}

@Cacheable이 거의 동일 + 더 깔끔.

함정

1. 직렬화 클래스 변경

@Cacheable("users")
public User get(Long id) { ... }

User 클래스 변경 → 옛 캐시 deserialize 실패. cache version 추가 또는 cache.clear.

2. 큰 키

redis.opsForValue().set("posts", listOf1MItems, ...)    // 거대 value

Redis O(N) 명령 + 메모리. 작은 단위로 분할 (posts:123 같이).

3. KEYS 명령

redis.keys("user:*")    // BLOCKING + O(N), 프로덕션 금지

SCAN 사용:

redis.execute((RedisCallback<Set<String>>) connection -> {
    Set<String> keys = new HashSet<>();
    Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions().match("user:*").count(1000).build());
    while (cursor.hasNext()) {
        keys.add(new String(cursor.next()));
    }
    return keys;
});

4. cluster mode + multi-key 명령

redis.opsForValue().multiGet(List.of("user:1", "user:2"));    // cluster에서 같은 slot이어야

{} notation으로 hash tag: user:{1}, user:{1}:detail.

5. TTL 없는 키

redis.opsForValue().set("key", "value");    // 영구 (메모리 누적)
redis.opsForValue().set("key", "value", Duration.ofHours(1));    // 권장

Reactive Redis

implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")
@Autowired
ReactiveRedisTemplate<String, String> reactive;

public Mono<String> get(String key) {
    return reactive.opsForValue().get(key);
}

WebFlux 환경.

Redis vs Memcached

RedisMemcached
데이터 구조String, List, Hash, Set, ZSet, StreamString만
영속RDB/AOFX
Pub/SubOX
클러스터O클라이언트 sharding
트랜잭션O (MULTI)X

Redis가 더 강력. Memcached는 단순 캐시만.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기