
docker-compose.yml - Redis Sentinel 구성
redis_master:
hostname: redis-master
container_name: redis-master
image: "bitnami/redis"
environment:
- REDIS_REPLICATION_MODE=master
- ALLOW_EMPTY_PASSWORD=yes
ports:
- "6379:6379"
redis_replica1:
hostname: redis-replicas-1
container_name: redis-replicas-1
image: "bitnami/redis"
environment:
- REDIS_REPLICATION_MODE=slave
- REDIS_MASTER_HOST=redis-master
- ALLOW_EMPTY_PASSWORD=yes
ports:
- "5000:6379"
depends_on:
- redis_master
redis-sentinel-1:
container_name: sentinel1
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=3000
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
ports:
- "26379:26379"
depends_on:
- redis_master
- redis_replica1
- redis_replica2
networks:
- t4y
redis-sentinel-2:
container_name: sentinel2
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=3000
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
ports:
- "26380:26379"
depends_on:
- redis_master
- redis_replica1
- redis_replica2
networks:
- t4y
redis-sentinel-3:
container_name: sentinel3
image: 'bitnami/redis-sentinel:latest'
environment:
- REDIS_SENTINEL_DOWN_AFTER_MILLISECONDS=3000
- REDIS_MASTER_HOST=redis-master
- REDIS_MASTER_PORT_NUMBER=6379
- REDIS_MASTER_SET=mymaster
- REDIS_SENTINEL_QUORUM=2
ports:
- "26381:26379"
depends_on:
- redis_master
- redis_replica1
- redis_replica2
networks:
- t4y
REDIS_SENTINEL_QUORUM=2: 장애 감지를 위해 최소 2개의 Sentinel이 필요@Configuration
public class RedisConfig {
@Autowired
private RedisSentinelProperties redisSentinelProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// Redis Sentinel 설정
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().readFrom(ReadFrom.REPLICA_PREFERRED).build();
final RedisSentinelProperties.RedisMasterProperties masterConfig = redisSentinelProperties.getMaster();
RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(masterConfig.getHost(), masterConfig.getPort());
redisSentinelProperties.getNodes().forEach(node -> {
String[] nodeInfo = node.split(":");
staticMasterReplicaConfiguration.addNode(nodeInfo[0], Integer.parseInt(nodeInfo[1]));
});
return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules(); // LocalDateTime 같은 타입을 위한 모듈 자동 등록
objectMapper.deactivateDefaultTyping(); // @class 제거
// Jackson2JsonRedisSerializer 설정
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
// 키는 String, 값은 JSON 형식으로 처리
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
return template;
}
}
@Data
@Component
@ConfigurationProperties(prefix = "spring.data.redis.sentinel")
public class RedisSentinelProperties {
private RedisMasterProperties master;
private List<String> nodes;
@Data
public static class RedisMasterProperties {
private String host;
private Integer port;
}
}
Master-Slave + Sentinel 구조 적용
ReadFrom.REPLICA_PREFERRED) → Redis가 다운되면 슬레이브가 읽기 처리.RedisSentinelProperties 클래스는 @ConfigurationProperties를 사용하여 Spring Boot가 자동으로 바인딩할 수 있도록 구성Set<RedisNode>로 변환 →sentinelConfig)에 추가@Configuration
@EnableCaching
public class RedisConfig {
public static final String CACHE_PREFIX = "deliveryCache";
@Autowired
private RedisSentinelProperties redisSentinelProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// Redis Sentinel 설정
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder().readFrom(ReadFrom.REPLICA_PREFERRED).build();
final RedisSentinelProperties.RedisMasterProperties masterConfig = redisSentinelProperties.getMaster();
RedisStaticMasterReplicaConfiguration staticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(masterConfig.getHost(), masterConfig.getPort());
redisSentinelProperties.getNodes().forEach(node -> {
String[] nodeInfo = node.split(":");
staticMasterReplicaConfiguration.addNode(nodeInfo[0], Integer.parseInt(nodeInfo[1]));
});
return new LettuceConnectionFactory(staticMasterReplicaConfiguration, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new JdkSerializationRedisSerializer());
return template;
}
@Bean
public RedisCacheManager cacheManager(
RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration configuration = RedisCacheConfiguration
.defaultCacheConfig()
.disableCachingNullValues()
.entryTtl(Duration.ofHours(2))
.computePrefixWith(CacheKeyPrefix.simple())
.serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
return RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(configuration)
.build();
}
}
RedisCacheManager의 역할
entryTtl(Duration.ofHours(2))) → 2시간 동안 캐싱된 데이터 유지.CacheKeyPrefix.simple()을 사용하여 캐시 키를 쉽게 관리.RedisCacheManager vs RedisTemplate 차이| 구분 | RedisCacheManager | RedisTemplate |
|---|---|---|
| 주요 목적 | Spring Cache 기능 제공 (@Cacheable, @CachePut) | Key-Value 직접 조작 |
| 설정 방식 | @EnableCaching & @Cacheable 어노테이션 사용 | redisTemplate.opsForValue().set(key, value) |
| 데이터 저장 | Spring Cache 시스템이 자동으로 관리 | 개발자가 직접 데이터 저장 및 조회 |
| TTL 설정 | entryTtl(Duration.ofHours(2)) 설정 가능 | redisTemplate.expire(key, 2, TimeUnit.HOURS) 사용 |
| 직렬화 방식 | JdkSerializationRedisSerializer 사용 (객체 직렬화) | Jackson2JsonRedisSerializer 사용 (JSON 저장) |
RedisCacheManager vs RedisTemplate 예시@Service
@RequiredArgsConstructor
public class DeliveryApplicationService {
private final DeliveryRepository deliveryRepository;
private final RedisTemplate<String, Object> redisTemplate;
@CachePut(value = "deliveryCache", key = "#deliveryId")
public DeliveryInfoResponseDto endDelivery(UUID deliveryId) {
log.info("배송 완료 요청을 처리합니다. 배송 ID: {}", deliveryId);
Delivery delivery = deliveryRepository.findById(deliveryId).orElseThrow(
() -> new CustomException(DeliveryErrorCode.DELIVERY_NOT_FOUND)
);
if (delivery.getDeliveryStatus() == DeliveryStatus.PENDING) {
throw new CustomException(DeliveryErrorCode.DELIVERY_NOT_STARTED);
}
if (delivery.getDeliveryStatus() == DeliveryStatus.DELIVERED) {
throw new CustomException(DeliveryErrorCode.DELIVERY_ALREADY_DELIVERED);
}
delivery.endDelivery();
Delivery savedDelivery = deliveryRepository.save(delivery);
return DeliveryMapper.convertEntityToInfoResponseDto(savedDelivery);
}
}
✅ @CachePut을 사용하여 Redis 캐시에 배송 정보 저장
@CachePut(value = "deliveryCache", key = "#deliveryId")@Transactional(readOnly = true)
public DeliveryInfoResponseDto getDeliveryInfo(UUID deliveryId) {
log.info("배송 정보 조회 요청을 처리합니다. 배송 ID: {}", deliveryId);
DeliveryInfoResponseDto cachedDeliveryInfo = (DeliveryInfoResponseDto) redisTemplate.opsForValue().get(
"deliveryCache::" + deliveryId
);
if (cachedDeliveryInfo != null) {
return cachedDeliveryInfo;
}
Delivery delivery = deliveryRepository.findById(deliveryId)
.orElseThrow(() -> new CustomException(DeliveryErrorCode.DELIVERY_NOT_FOUND));
return DeliveryMapper.convertEntityToInfoResponseDto(delivery);
}
application.ymlspring:
data:
redis:
sentinel:
master:
host: localhost
port: 6379
nodes:
- localhost:26379
- localhost:26380
- localhost:26381
timeout: 10000
nodes?
- 애플리케이션이 Sentinel들과 통신하여 Redis 마스터 정보를 동적으로 가져올 수 있도록 하는 설정
- Redis Sentinel에 연결할 때 사용할 Sentinel 서버들의 주소를 정의
💡 즉, nodes를 설정하면 마스터가 변경되더라도 Sentinel을 통해 자동으로 새로운 마스터를 감지할 수 있다.