백엔드 데이터 인프라 73편 — Spring Data Redis (RedisTemplate · @Cacheable)

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 73편. Spring Data Redis — Spring Boot 와 Redis 통합. RedisTemplate·StringRedisTemplate·ReactiveRedisTemplate, @Cacheable·@CachePut·@CacheEvict 어노테이션, Lettuce vs Jedis 선택, Sentinel·Cluster 통합 설정까지 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 73편 — Spring Data Redis (RedisTemplate · @Cacheable)

이 글은 백엔드 데이터 인프라 시리즈 130편 중 73편이에요. 72편까지 Redis 운영·최적화를 풀었으니, 이번 73편에서는 자바·코틀린 백엔드가 가장 자주 쓰는 환경인 Spring Data Redis 통합을 다뤄요. Part 4-6의 마지막 글이고, 시리즈 1 자바 백엔드 입문 59편과도 자연스럽게 이어지는 자리예요.

Spring Data Redis가 어렵게 느껴지는 이유

Spring 추상화가 너무 많아서 처음 보면 어디서 시작할지 잡히지 않아요.

첫째, 선택지가 많아요. Lettuce(Netty 기반 비동기 Redis 클라이언트)와 Jedis(Spring 이전 표준이던 동기 클라이언트) 중에서 한 번, RedisTemplate·StringRedisTemplate·ReactiveRedisTemplate 중에서 또 한 번, @Cacheable로 자동 캐싱할지 RedisTemplate으로 직접 명령을 박을지에서 한 번, @RedisHash repository를 쓸지 직접 명령을 쓸지에서 또 한 번 — 어느 조합을 골라야 할지 처음에는 막막하죠.

둘째, 자동 직렬화가 함정이에요. 기본 직렬화기가 JDK Serializable(자바 표준 직렬화 포맷)이라 사람이 읽기 힘든 바이너리가 박혀요. redis-cli로 키를 들여다보면 깨진 문자열이 나오고, "내가 박은 값이 왜 이래?" 하면서 첫 삽에서 막히는 패턴이 흔합니다.

이 글에서는 5가지 통합 방식 비교, 직렬화 표준 패턴, Cache abstraction 어노테이션, Sentinel(Redis 고가용성 감시 데몬)·Cluster 설정까지 한 번에 정리할게요.

5가지 통합 방식

방식 자동화 추상화 자주 쓰는 자리
@Cacheable (Cache abstraction) 가장 높음 메서드 결과 자동 캐싱
RedisTemplate 중간 직접 명령 호출
StringRedisTemplate 중간 문자열만 다룰 때
ReactiveRedisTemplate 중간 비동기 (WebFlux)
@RedisHash Repository 높음 객체를 Hash 로 자동 매핑

실무 대부분은 @Cacheable 70%에 RedisTemplate 30% 비중이에요.

Cache Abstraction — @Cacheable

가장 흔한 자리예요. 메서드 결과를 자동으로 캐싱하고 다음 호출에서 그대로 돌려줍니다.

의존성 + 설정

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
spring:
  cache:
    type: redis
  data:
    redis:
      host: localhost
      port: 6379
@Configuration
@EnableCaching
public class CacheConfig { }

어노테이션 3종

@Service
public class UserService {

    @Cacheable(value = "users", key = "#uid")
    public User getUser(Long uid) {
        // cache miss → 이 메서드 실행 + 결과 캐싱
        return userRepo.findById(uid).orElseThrow();
    }

    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        // 항상 실행 + 결과 캐시 갱신
        return userRepo.save(user);
    }

    @CacheEvict(value = "users", key = "#uid")
    public void deleteUser(Long uid) {
        // 캐시 무효화
        userRepo.deleteById(uid);
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearAllUsers() {
        // users:* 전부 무효화
    }
}
  • @Cacheable — cache-aside(읽을 때 캐시 먼저 보고 없으면 원본 조회) 패턴을 자동으로 박아줘요.
  • @CachePut — 항상 메서드를 실행하고 결과로 캐시를 갱신해요.
  • @CacheEvict — 특정 키 또는 전체를 무효화해요.

TTL 설정

기본값은 영구 보관이에요. TTL을 박으려면 Cache Manager를 커스텀합니다.

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory cf) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofHours(1))
        .serializeValuesWith(SerializationPair.fromSerializer(
            new GenericJackson2JsonRedisSerializer()))
        .disableCachingNullValues();

    return RedisCacheManager.builder(cf)
        .cacheDefaults(config)
        .build();
}

disableCachingNullValues()null 결과를 캐시하지 않게 막아줘요. DB miss가 캐시에 박히면 영구히 null만 돌려주는 함정이 생깁니다.

RedisTemplate — 직접 명령

복잡한 자료구조와 명령은 RedisTemplate으로 직접 박는 편이 깔끔해요.

의존성

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

사용

@Service
public class RankingService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    public void incrementScore(String user, double delta) {
        redisTemplate.opsForZSet().incrementScore("leaderboard", user, delta);
    }

    public Set<ZSetOperations.TypedTuple<String>> top10() {
        return redisTemplate.opsForZSet()
            .reverseRangeWithScores("leaderboard", 0, 9);
    }

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

Operations 종류

Operations 자료구조
opsForValue() String
opsForHash() Hash
opsForList() List
opsForSet() Set
opsForZSet() Sorted Set
opsForStream() Stream
opsForGeo() Geospatial
opsForCluster() Cluster 명령

직렬화 — 가장 흔한 함정

여기서 시험 함정이 하나 있어요. 기본 직렬화기가 JdkSerializationRedisSerializer라 사람이 못 읽는 바이너리가 박힙니다.

> GET user:42
"\xac\xed\x00\x05sr\x00..."     # 깨진 듯한 출력

표준 패턴 — JSON 직렬화

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

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

    template.setKeySerializer(keySerializer);
    template.setHashKeySerializer(keySerializer);
    template.setValueSerializer(valueSerializer);
    template.setHashValueSerializer(valueSerializer);
    return template;
}

이렇게 박아두면 redis-cli에서도 사람이 읽을 수 있는 JSON으로 보입니다.

StringRedisTemplate — 모두 String

문자열만 다룰 때는 더 단순하게 갈 수 있어요.

@Autowired
private StringRedisTemplate stringRedisTemplate;

public void setSession(String sid, String value) {
    stringRedisTemplate.opsForValue().set(sid, value, Duration.ofHours(1));
}

자동으로 String 직렬화가 걸리니까 별도 설정 없이도 사람 친화적이에요.

Lettuce vs Jedis — 선택 기준

Spring Boot 2.0+의 기본은 Lettuce예요.

항목 Lettuce Jedis
비동기 / Reactive X
Connection 모델 공유 (Netty) 인스턴스당 connection
Thread-safe X (pool 필요)
Performance 일반적으로 ◯ 단순 환경에서는 비슷
학습 곡선 약간 높음 낮음
기본값 (Spring Boot 2+) X

기본 권장은 Lettuce예요. 비동기·WebFlux(Spring의 논블로킹 웹 스택) 환경에서는 사실상 필수입니다.

Jedis 강제 사용

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<!-- Lettuce 제외 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Sentinel 통합

spring:
  data:
    redis:
      sentinel:
        master: mymaster
        nodes:
          - sentinel-1:26379
          - sentinel-2:26379
          - sentinel-3:26379
      password: redis-pass

코드는 그대로 두면 RedisTemplate이 자동으로 라우팅해줘요.

Cluster 통합

spring:
  data:
    redis:
      cluster:
        nodes:
          - cluster-node-1:7000
          - cluster-node-2:7000
          - cluster-node-3:7000
        max-redirects: 3
      lettuce:
        cluster:
          refresh:
            adaptive: true
            period: 60s

adaptive: true로 두면 MOVED(키가 다른 슬롯으로 옮겨졌다는 응답)를 감지했을 때 topology를 자동으로 새로 받아옵니다. 운영 환경 표준 설정이에요.

Pub/Sub 통합

Pub/Sub은 Redis의 발행·구독 메시징 채널이에요.

@Configuration
public class RedisPubSubConfig {

    @Bean
    public RedisMessageListenerContainer container(
            RedisConnectionFactory cf, MessageListenerAdapter adapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(cf);
        container.addMessageListener(adapter, new ChannelTopic("notifications"));
        return container;
    }

    @Bean
    public MessageListenerAdapter adapter(MyMessageHandler handler) {
        return new MessageListenerAdapter(handler, "handleMessage");
    }
}

@Component
public class MyMessageHandler {
    public void handleMessage(String message, String channel) {
        // 처리
    }
}

@RedisHash Repository — Hash 자동 매핑

@RedisHash("users")
public class User {
    @Id
    private Long id;
    private String name;
    @Indexed
    private String email;
    @TimeToLive
    private Long ttl;
}

public interface UserRepository extends CrudRepository<User, Long> {
    List<User> findByEmail(String email);    // @Indexed 덕분에 보조 인덱스
}
@Autowired
private UserRepository userRepository;

userRepository.save(new User(42L, "Alice", "alice@..."));
User u = userRepository.findById(42L).orElseThrow();

객체-Hash 자동 매핑과 보조 인덱스가 장점이에요. 다만 복잡한 쿼리는 어렵고, RDB JPA Repository만큼 강력하지는 않다는 점은 감수해야 해요.

한계·실무 함정

1. 기본 직렬화 = Jdk

위에서 강조한 그 함정이에요. JSON 직렬화로 전환은 사실상 필수예요.

2. null 캐시 함정

@Cacheable은 null 결과도 캐시에 박아둬요. 그러면 그 키는 영구히 null만 돌려줍니다. disableCachingNullValues()로 막거나 unless = "#result == null"로 거르세요.

3. TTL 안 박힘

기본 TTL이 영구라서, 영구 보관 의도가 아니라면 반드시 TTL을 박아둬야 해요.

4. Lettuce connection 잘못된 사용

Lettuce는 connection을 공유합니다. 한 connection에 blocking 명령을 박으면 그 connection을 쓰는 호출이 전부 멈춰요. BLPOP(리스트가 빌 때까지 대기하는 blocking pop)처럼 막히는 명령은 별도 connection으로 분리해야 합니다.

5. Cluster 환경 multi-key 호출

RedisTemplate.opsForValue().multiGet(keys)는 키들이 서로 다른 슬롯에 흩어져 있으면 CROSSSLOT 에러(여러 슬롯에 걸친 명령 거부)로 깨질 수 있어요. hash tag로 같은 슬롯에 모으거나 Pipelining으로 키를 나눠서 보내세요.

시험 직전 한 번 더 — Spring Data Redis 함정 압축 노트

  • 의존성 = spring-boot-starter-data-redis
  • 5가지 통합 방식 = @Cacheable · RedisTemplate · StringRedisTemplate · ReactiveRedisTemplate · @RedisHash
  • 가장 흔한 = @Cacheable 70% + RedisTemplate 30%
  • @Cacheable = cache-aside 자동, 메서드 결과 캐싱
  • @CachePut = 항상 실행 + 결과 갱신
  • @CacheEvict = 캐시 무효화
  • TTL 기본 = 영구 → Cache Manager 커스텀 필수
  • disableCachingNullValues() = null 캐시 함정 방지
  • RedisTemplate Operations = opsForValue/Hash/List/Set/ZSet/Stream/Geo/Cluster
  • 기본 직렬화 = JdkSerializationRedisSerializer (사람 읽기 어려움)
  • JSON 직렬화 표준 = StringRedisSerializer key + GenericJackson2JsonRedisSerializer value
  • StringRedisTemplate = String 전용 (자동 String 직렬화)
  • Lettuce = Spring Boot 2+ 기본, 공유 connection, 비동기 지원
  • Jedis = 인스턴스당 connection, pool 필요, 단순
  • Sentinel 통합 = spring.data.redis.sentinel.master + nodes
  • Cluster 통합 = spring.data.redis.cluster.nodes + lettuce.cluster.refresh.adaptive=true
  • Pub/Sub = RedisMessageListenerContainer + MessageListenerAdapter
  • @RedisHash Repository = 객체 ↔ Hash 자동 매핑, @Indexed 보조 인덱스
  • 함정 — 기본 Jdk 직렬화 → 사람 못 읽음, JSON 으로 전환
  • 함정 — null 결과 캐시 → 영구 null 응답
  • 함정 — TTL 안 박음 → 영구 보관
  • 함정 — Lettuce 공유 connection 에 blocking 명령 → 전체 멈춤
  • 함정 — Cluster multiGet → CROSSSLOT, hash tag 또는 Pipelining 분할

공식 문서: Spring Data RedisRedis Spring Integration 에서 자세한 사양을 확인할 수 있어요.

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!