Spring Boot Redis 연동
Spring Boot에서 Redis를 사용할 때는 어떤 추상화로 접근할지, 어떤 serializer를 쓸지, 장애 시 어떻게 fallback할지를 먼저 정해야 합니다.
용어
| 용어 | 의미 |
|---|---|
| Spring Data Redis | Spring에서 Redis 접근을 돕는 프로젝트 |
| RedisTemplate | Redis 자료구조를 범용으로 다루는 template |
| StringRedisTemplate | key/value를 문자열 중심으로 다루는 template |
| ReactiveRedisTemplate | reactive stack에서 사용하는 Redis template |
| Spring Cache | @Cacheable, @CacheEvict 기반 캐시 추상화 |
| Redis Repository | Redis Hash 기반 repository 추상화 |
| Lettuce | 기본적으로 많이 쓰이는 Netty 기반 Redis client |
| Jedis | 전통적인 Redis client |
| Redisson | 분산 락, 자료구조 API를 제공하는 client |
질문
RedisTemplate과 Spring Cache는 언제 다르게 쓰나?
| 구분 | RedisTemplate | Spring Cache |
|---|---|---|
| 제어 | 명령과 자료구조를 직접 제어 | annotation 기반 캐시 |
| 사용처 | 카운터, 락, 랭킹, custom key | 조회 결과 캐시 |
| 장점 | 세밀한 제어 | 코드가 간결 |
| 단점 | 구현 코드 증가 | 복잡한 캐시 정책은 숨겨져 보일 수 있음 |
의존성 설정
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
연결 설정
spring:
data:
redis:
host: localhost
port: 6379
timeout: 2s
lettuce:
pool:
max-active: 16
max-idle: 16
min-idle: 2
max-wait: 500ms
운영에서는 host, port뿐 아니라 command timeout, connect timeout, pool, retry, circuit breaker를 함께 봅니다.
| 설정 | 의미 | 기준 |
|---|---|---|
timeout |
Redis command timeout | API timeout보다 짧게 |
max-active |
pool에서 사용할 최대 연결 수 | 인스턴스 수와 Redis maxclients 기준 |
max-wait |
pool 대기 시간 | 길면 장애 전파, 짧으면 빠른 실패 |
min-idle |
유지할 idle 연결 수 | 급격한 트래픽 대비 |
RedisTemplate
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
RedisTemplate은 다양한 자료구조를 세밀하게 다룰 때 좋습니다.
StringRedisTemplate
stringRedisTemplate.opsForValue().set("auth:code:user-1", "123456", Duration.ofMinutes(3));
String code = stringRedisTemplate.opsForValue().get("auth:code:user-1");
문자열 key/value만 다룬다면 StringRedisTemplate이 단순합니다.
Spring Cache Abstraction
@Cacheable(cacheNames = "product", key = "#productId")
public ProductResponse getProduct(Long productId) {
return productRepository.findById(productId)
.map(ProductResponse::from)
.orElseThrow();
}
@CacheEvict(cacheNames = "product", key = "#productId")
public void updateProduct(Long productId, ProductUpdateRequest request) {
// DB update
}
조회 캐시는 Spring Cache로 시작하기 좋지만, TTL, key prefix, null cache, lock 기반 stampede 방지까지 필요하면 설정을 명확히 봐야 합니다.
CacheManager TTL 설정
cache 이름별로 TTL을 다르게 두면 운영 기준이 명확해집니다.
@Bean
RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
);
Map<String, RedisCacheConfiguration> configs = Map.of(
"product", defaultConfig.entryTtl(Duration.ofMinutes(5)),
"category", defaultConfig.entryTtl(Duration.ofHours(1)),
"notice", defaultConfig.entryTtl(Duration.ofMinutes(30))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(configs)
.build();
}
| cache | TTL 기준 |
|---|---|
product |
가격·상태 변경 가능성이 있어 짧게 |
category |
변경이 적어 길게 |
notice |
운영자가 수정할 수 있어 중간 정도 |
Null Cache 주의
Spring Cache에서 null caching을 켜면 cache penetration을 줄일 수 있지만, 존재하지 않는 값도 Redis에 저장됩니다. null cache가 필요하면 짧은 TTL을 따로 두거나, 서비스 코드에서 명시적인 EMPTY value를 저장하는 방식을 검토합니다.
Redis Repository
Redis Repository는 객체를 Redis Hash로 저장하는 추상화입니다. 세션성 객체나 짧게 살아도 되는 상태에는 쓸 수 있지만, RDBMS repository처럼 원장 데이터를 맡기면 위험합니다.
Serializer 전략
| Serializer | 특징 | 주의 |
|---|---|---|
StringRedisSerializer |
문자열 key/value에 적합 | 객체 저장에는 직접 변환 필요 |
GenericJackson2JsonRedisSerializer |
타입 정보 포함 JSON | payload가 커질 수 있음 |
Jackson2JsonRedisSerializer |
특정 타입 JSON | 타입별 설정 필요 |
JdkSerializationRedisSerializer |
Java 직렬화 | 사람이 읽기 어렵고 호환성·보안 주의 |
주의: 운영 중 serializer를 바꾸면 기존 Redis 값과 호환되지 않을 수 있다. key version을 나누거나 마이그레이션 계획을 둔다.
Lettuce vs Jedis vs Redisson
| Client | 특징 | 언제 |
|---|---|---|
| Lettuce | 비동기·Netty 기반, Spring Boot 기본 선택지로 자주 사용 | 일반 Redis 연동 |
| Jedis | 단순한 동기 client | 단순 환경, 기존 코드 |
| Redisson | 분산 락, map, queue 같은 고수준 API | 락과 고급 구조가 필요할 때 |
Connection Pool과 Timeout
| 설정 | 기준 |
|---|---|
| connection pool | 인스턴스 수와 동시 요청량 기준 |
| command timeout | Redis 지연 시 애플리케이션 thread가 오래 묶이지 않게 |
| retry | 짧고 제한적으로, 폭주 방지 |
| circuit breaker | Redis 장애가 전체 서비스 장애로 번지지 않게 |
Lettuce Client 설정 예시
@Bean
LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() {
return builder -> builder
.commandTimeout(Duration.ofSeconds(2))
.shutdownTimeout(Duration.ofMillis(100));
}
Redis timeout은 DB timeout, API timeout과 함께 맞춰야 합니다. Redis가 캐시라면 Redis timeout이 길어서 전체 API가 느려지는 상황을 피하는 것이 보통입니다.
Retry와 Circuit Breaker
Redis 장애에서 무제한 retry는 장애를 키웁니다.
| 상황 | 권장 |
|---|---|
| 조회 캐시 실패 | 짧게 실패 후 DB fallback |
| 락 획득 실패 | 제한 횟수 retry 후 실패 응답 |
| rate limit Redis 실패 | 보안 요구에 따라 fail-open/fail-closed 결정 |
| Redis 전체 장애 | circuit breaker로 일정 시간 호출 차단 |
Redis 장애 시 Fallback
| 상황 | 대응 |
|---|---|
| 조회 캐시 Redis 장애 | DB 직접 조회, rate limit 필요 |
| 세션 Redis 장애 | 재로그인 허용 여부 결정 |
| 락 Redis 장애 | 작업 중단 또는 DB unique로 보호 |
| rate limit Redis 장애 | fail-open/fail-closed 정책 결정 |
public ProductResponse getProduct(Long productId) {
try {
ProductResponse cached = cacheReader.get(productId);
if (cached != null) {
return cached;
}
} catch (RedisConnectionFailureException ex) {
// Redis 장애 시 DB fallback
}
ProductResponse response = productRepository.findResponse(productId);
cacheWriter.put(productId, response);
return response;
}
fallback을 넣을 때는 DB가 갑자기 모든 트래픽을 받게 될 수 있습니다. Redis 장애 시 DB 보호를 위해 rate limit, degraded response, circuit breaker를 함께 고려합니다.
테스트 전략
| 방식 | 특징 |
|---|---|
| Local Redis | 빠르고 단순 |
| Docker Redis | 개발 환경 일관성 |
| Testcontainers | 통합 테스트 재현성 |
| Embedded Redis | 환경에 따라 유지보수 이슈 가능 |
Testcontainers 예시
@Testcontainers
@SpringBootTest
class RedisCacheTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7")
.withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
StringRedisTemplate redisTemplate;
@Test
void saveWithTtl() {
redisTemplate.opsForValue().set("auth:code:user-1", "123456", Duration.ofMinutes(3));
Long ttl = redisTemplate.getExpire("auth:code:user-1");
assertThat(ttl).isPositive();
}
}
테스트에서는 단순 저장 성공뿐 아니라 TTL, serializer, key prefix, cache eviction까지 확인하는 것이 좋습니다.
베스트 프랙티스
| 권장 방식 | 이유 |
|---|---|
| key와 TTL 정책을 코드 밖 설정으로 관리 | 운영 변경이 쉬움 |
| serializer를 명시 | 기본 직렬화 의존 방지 |
| timeout을 짧고 명확히 설정 | Redis 장애 전파 방지 |
| fallback 정책 문서화 | 장애 시 동작 예측 |
| Testcontainers로 통합 테스트 | 실제 Redis 명령 검증 |
실무에서는?
| 사용처 | 추천 접근 |
|---|---|
| 단순 조회 캐시 | Spring Cache |
| 인증번호·토큰 | StringRedisTemplate |
| 랭킹·카운터 | RedisTemplate |
| 분산 락 | Redisson 또는 SET NX PX 직접 구현 |
| 장애 대응 | timeout + fallback + metric |
관련 파일: - 캐시 전략과 정합성 - 트랜잭션과 동시성 - 실무 유즈케이스