에디블로그
Engineer's Field Notes

AI 자동화로 매일
한 편씩 쓰는
엔지니어 운영 노트

Claude Code · 자동화 파이프라인 · 사고 회고까지. 잘 굴러간 기록 + 깨진 흔적도 같이 남깁니다.

사람이 할 수 있는 일은,
AI도 할 수 있어야 합니다.
매일 한 편 쓰면서 검증 중.
— 이번 주 가장 많이 읽힌 글 TOP 3
백엔드/캐시

[Redis] Spring Data Redis 설정: RedisTemplate 직렬화, StringRedisTemplate 차이, 커넥션 타임아웃

반응형
[Redis] Spring Data Redis 설정: RedisTemplate 직렬화, StringRedisTemplate 차이, 커넥션 타임아웃

[Redis] Spring Data Redis 설정: RedisTemplate 직렬화, StringRedisTemplate 차이, 커넥션 타임아웃

스프링에서 Redis는 RedisTemplate 하나로 거의 다 다뤄요. 그런데 처음 쓰면 꼭 만나는 벽이 있어요. Redis CLI로 보면 키가 \xac\xed\x00\x05... 같은 이상한 바이트로 저장돼 있는 거예요. 이건 직렬화 설정을 안 만져서 생기는 일이에요.

Redis 시리즈의 기본기를 다지는 첫 편이에요. Spring Data Redis 구성과 커넥션을 먼저 보고, RedisTemplateStringRedisTemplate이 어떻게 다른지, 가장 중요한 직렬화는 어떻게 잡는지, 자료구조별로는 어떻게 쓰는지, 그리고 자주 만나는 문제는 무엇인지 순서대로 짚어요.

01. Spring Data Redis 구성

먼저 의존성을 추가해요. spring-boot-starter-data-redis를 넣으면 기본 커넥션 라이브러리로 Lettuce가 따라와요.

spring:
  data:
    redis:
      host: localhost
      port: 6379

커넥션 라이브러리는 Lettuce와 Jedis 두 가지가 있어요. Lettuce는 Netty 기반의 논블로킹이고 스레드 세이프해서 기본으로 권장돼요. Jedis는 전통적인 방식인데, 멀티 스레드에서 커넥션 풀을 잘 관리해야 해요. 특별한 이유가 없으면 기본 Lettuce를 그대로 써요.

02. RedisTemplate vs StringRedisTemplate

Redis를 다루는 진입점은 두 가지예요. RedisTemplateStringRedisTemplate이에요.

Spring Data Redis 진입점 선택. StringRedisTemplate은 키와 값이 모두 문자열이라 단순 값에 깔끔하고, RedisTemplate은 자료구조를 세밀하게 제어하며, RedisHash Repository는 객체 CRUD에 편하다
  • StringRedisTemplate — 키와 값을 모두 문자열로 다뤄요. 직렬화가 문자열로 고정돼 있어서 값이 사람이 읽는 그대로 저장돼요.
  • RedisTemplate<K, V> — 임의의 객체를 다뤄요. 대신 직렬화를 직접 설정해야 해요.

단순 문자열·숫자만 다루면 StringRedisTemplate이 제일 깔끔해요. 객체를 통째로 저장하려면 RedisTemplate을 쓰되, 직렬화를 반드시 설정해야 해요. 여기서 안 만지면 다음에 볼 그 이상한 바이트 문제가 생겨요.

03. 직렬화, 가장 큰 함정

RedisTemplate의 기본 직렬화기는 JdkSerializationRedisSerializer예요. 자바 기본 직렬화를 쓰는데, 결과가 사람이 못 읽는 바이트라 Redis CLI로 보면 깨져 보여요. 게다가 자바 직렬화라 다른 언어에서 못 읽고 클래스가 바뀌면 깨지기 쉬워요.

그래서 직렬화기를 명시적으로 바꿔요. 키는 문자열로, 값은 JSON으로 두는 게 가장 흔한 설정이에요.

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
    var template = new RedisTemplate<String, Object>();
    template.setConnectionFactory(cf);

    var keySerializer = new StringRedisSerializer();
    var valueSerializer = new GenericJackson2JsonRedisSerializer();

    template.setKeySerializer(keySerializer);
    template.setValueSerializer(valueSerializer);
    template.setHashKeySerializer(keySerializer);
    template.setHashValueSerializer(valueSerializer);
    return template;
}
RedisTemplate 직렬화기 4개. key와 value 직렬화기만 설정하고 hashKey와 hashValue를 빼먹으면 Hash 자료구조를 쓸 때만 JDK 직렬화 바이트로 깨진다
여기서 놓치기 쉬운 함정 — 직렬화기는 네 가지를 다 설정해야 해요. key, value만 설정하고 hashKey, hashValue를 빼면, String 자료구조는 멀쩡한데 opsForHash()를 쓰는 순간 그 부분만 기본 JDK 직렬화로 깨져요. "어떤 키는 잘 읽히는데 어떤 키는 바이트로 보여요"의 정체가 대개 이거예요.

한글과 날짜 문제

StringRedisSerializer는 기본이 UTF-8이라 한글은 보통 괜찮아요. 문제는 날짜예요. LocalDateTime 같은 자바 8 시간 타입은 Jackson이 기본으로 직렬화를 못 해서 예외가 나요. 이때는 JavaTimeModule을 등록한 ObjectMapper를 직렬화기에 넣어줘야 해요. 객체에 날짜 필드가 있으면 거의 항상 만나는 문제예요.

다만 커스텀 ObjectMapper를 넘기면 기본 생성자가 켜주던 타입 정보 기록(default typing)이 빠져서, Object 역직렬화가 LinkedHashMap으로 나올 수 있어요. activateDefaultTyping까지 챙겨요.

04. 자료구조별 사용법

Redis는 단순 key-value가 아니라 여러 자료구조를 줘요. RedisTemplate은 자료구조마다 전용 Operations를 줘요.

  • opsForValue() — String. 단순 값, 카운터, 캐시.
  • opsForHash() — Hash. 객체의 필드들을 묶어 저장.
  • opsForList() — List. 큐·스택.
  • opsForSet() — Set. 중복 없는 모음, 태그.
  • opsForZSet() — Sorted Set. 점수 기반 정렬, 랭킹.
// String — 값 저장 + TTL
redisTemplate.opsForValue().set("user:1", user, Duration.ofMinutes(10));

// Hash — 필드 단위 저장 (위 String과 다른 키를 써야 해요 — 같은 키에 다른 타입은 WRONGTYPE 에러)
redisTemplate.opsForHash().put("user:1:profile", "name", "jess");

// Sorted Set — 랭킹
redisTemplate.opsForZSet().add("ranking", "user:1", 100.0);

자료구조를 잘 고르면 코드가 단순해져요. 랭킹을 직접 정렬하지 말고 Sorted Set에 맡기고, 객체의 일부 필드만 자주 바꾸면 Hash로 두는 식이에요. 자료구조별 실무 패턴은 자료구조 활용 편에서 더 다뤄요.

05. RedisTemplate 말고 Repository로도 써요

객체를 통째로 저장하고 조회하는 패턴이라면, RedisTemplate 대신 Spring Data의 Repository 추상화를 쓸 수도 있어요. JPA Repository와 비슷한 느낌이에요.

@RedisHash(value = "user", timeToLive = 600)  // TTL 10분
public class User {
    @Id private String id;
    private String name;
}

public interface UserRedisRepository extends CrudRepository<User, String> { }

@RedisHash를 붙인 객체는 Hash로 저장되고, save·findById 같은 메서드를 그냥 쓸 수 있어요. @Indexed를 필드에 붙이면 그 필드로 조회도 돼요.

둘의 선택 기준은 이래요. 자료구조를 직접 다루거나 세밀하게 제어해야 하면 RedisTemplate, 객체를 CRUD로 단순하게 저장·조회하면 Repository가 편해요. 캐시처럼 값 단위로 다루는 경우엔 보통 RedisTemplate이나 다음에 볼 @Cacheable을 더 많이 써요.

06. 커넥션 설정

운영에 올리려면 커넥션 풀과 타임아웃을 챙겨야 해요. Lettuce는 커넥션을 공유하지만 풀을 쓰려면 commons-pool2를 추가하고 설정해요.

spring:
  data:
    redis:
      host: localhost
      port: 6379
      timeout: 2s
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
Redis 타임아웃 미설정의 장애 전파. Redis 응답이 지연되면 요청 스레드가 무한정 대기해 스레드 풀이 고갈되고 전체 서비스 지연으로 번진다. 타임아웃과 DB fallback을 두면 Redis가 죽어도 서비스는 버틴다
타임아웃은 꼭 잡아요. <code>spring.data.redis.timeout</code>은 명령 응답 타임아웃이고, 연결 수립은 <code>connect-timeout</code>이 따로 있어요. Redis가 느려지거나 응답을 안 할 때 타임아웃이 없으면 요청 스레드가 무한정 매달려요. 그 스레드들이 쌓여 풀이 고갈되면, "캐시 한 번 느려진 것"이 "전체 서비스 장애"가 돼요. Redis를 캐시로 쓸수록 "Redis가 죽어도 서비스는 버텨야 한다"를 전제로 타임아웃과 원본(DB) 조회 fallback을 설계해요.

07. 자주 만나는 문제

Redis에 값이 이상한 바이트로 저장돼요

기본 JdkSerializationRedisSerializer를 그대로 쓴 거예요. 키는 StringRedisSerializer, 값은 GenericJackson2JsonRedisSerializer로 바꾸면 사람이 읽는 형태로 저장돼요.

Hash만 깨져요

key·value 직렬화기만 설정하고 hashKey·hashValue를 빼먹은 거예요. 네 개를 다 설정해야 해요.

LocalDateTime 저장에서 예외가 나요

Jackson이 자바 8 시간 타입을 기본으로 못 다뤄서예요. JavaTimeModule을 등록한 ObjectMapper를 직렬화기에 넣어요.

Redis가 느려지면 서비스 전체가 느려져요

타임아웃을 안 잡은 거예요. timeout을 설정하고, 캐시 실패 시 원본 조회로 넘어가는 fallback을 둬요.

정리

Spring Data Redis의 시작은 RedisTemplate과 직렬화 설정이에요. 무엇을 다루느냐로 진입점이 갈려요. 단순 문자열·숫자만 다루면 StringRedisTemplate으로 충분하고, 객체를 통째로 넣어야 하면 RedisTemplate에 명시적 직렬화기(키는 String, 값은 JSON, 네 개를 다)를 걸어요. 그래야 사람이 읽는 형태로 일관되게 저장돼요. 여기에 날짜 타입과 커넥션 타임아웃까지 챙기면 운영에 올릴 기본기가 갖춰져요.

이게 Redis 시리즈의 첫 단추예요. 직렬화로 다진 기본기 위에서 캐싱(@Cacheable), 자료구조 활용, 분산 락으로 하나씩 넓혀가요.

출처: Spring Data Redis — RedisTemplate · Spring Data Redis — Serializers · Redis — Data Types

반응형

📚 같이 보면 좋은

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."