MSA 기반 프로젝트를 진행하면서, 각 마이크로서비스가 인증 처리를 위해 하나의 레디스를 공유하도록 구성할 필요가 있었습니다. 하지만 단일 레디스로는 장애 발생 시 전체 서비스에 영향을 줄 수 있어, 고가용성을 확보할 수 있는 구조가 필요했습니다.
이 고민을 하던 중 카카오페이 기술 블로그 Redis on Kubernetes 플랫폼을 구성해 나가기를 접하게 되었고, 이를 참고하여 쿠버네티스 환경에서 레디스 클러스터를 직접 구성해보기로 했습니다.
이 글에서는 Redis Cluster 구축부터 Spring Boot 애플리케이션과의 연동까지, 전체 과정을 정리해보려 합니다.

1) Redis StatefulSet
레디스를 쿠버네티스 환경에 클러스터 형태로 배포하려면 가장 먼저 구성해야 할 리소스가 StatefulSet입니다.
Deployment는 pod를 생성할 때 이름이나 스토리지가 바뀔 수 있어 상태를 유지해야 하는 애플리케이션에는 적합하지 않습니다. 반면 StatefulSet은 각 pod에 고유한 이름과 네트워크 주소, 그리고 영구 볼륨을 연결하기 때문에 레디스처럼 상태를 저장해야 하는 서비스에 적합합니다.
이번 구성에서는 레디스 노드를 3개로 설정해 dev-redis-token-0, dev-redis-token-1, dev-redis-token-2라는 고정된 이름을 갖도록 만들었습니다. 이렇게 하면 클러스터를 구성할 때 각 노드를 정확하게 지정할 수 있고, 노드 간 통신도 DNS 주소를 통해 안정적으로 이루어집니다.
또한, 각 레디스 노드는 /data 경로에 클러스터 정보와 데이터를 저장하게 되는데, 이를 위한 영구 저장소를 PersistentVolumeClaim으로 연결했습니다. pod가 재시작되더라도 데이터는 유지되므로 안정적인 운영이 가능합니다.
레디스 설정은 redis.conf 파일을 ConfigMap으로 관리해 외부에서 주입합니다. 이를 통해 이미지 자체를 수정하지 않고도 설정만 바꿔서 쉽게 유지보수할 수 있게 했습니다.
# dev-redis-stateful.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: dev-redis-token
spec:
# 이 StatefulSet이 의존할 Headless Service 이름
serviceName: "dev-redis-token"
# Redis 노드 수 (3개 클러스터 구성)
replicas: 3
selector:
matchLabels:
app: dev-redis-token
template:
metadata:
labels:
app: dev-redis-token
spec:
containers:
- name: redis
image: redis:7.0-alpine
# ConfigMap에 정의된 redis.conf 파일로 Redis 서버 실행
command:
- redis-server
- "/redis-config/redis.conf"
ports:
# Redis 기본 포트 (클라이언트 요청 처리)
- containerPort: 6379
name: redis
# Redis 클러스터 노드 간 통신용 포트
- containerPort: 16379
name: cluster
volumeMounts:
# Redis 설정 파일을 담을 ConfigMap 마운트
- name: redis-config
mountPath: /redis-config
# Redis 데이터 저장소 (AOF, 클러스터 설정 등)
- name: redis-data
mountPath: /data
volumes:
# redis.conf 설정을 주입받기 위한 ConfigMap 볼륨
- name: redis-config
configMap:
name: dev-redis-token-config
volumeClaimTemplates:
# 각 Pod별로 독립적인 PVC 생성 (예: redis-data-dev-redis-token-0)
- metadata:
name: redis-data
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
2) Redis 설정 ConfigMap
레디스를 클러스터 모드로 실행하려면 기본 이미지로는 부족하고, 몇 가지 필수 설정들을 적용해줘야 합니다. 쿠버네티스에서는 ConfigMap을 사용해 설정 파일을 외부에서 주입할 수 있어, 운영 중에도 설정을 쉽게 바꾸고 애플리케이션 이미지와 설정을 분리할 수 있어 유연한 운영이 가능합니다.
설정 내용에는 클러스터를 활성화하는 cluster-enabled yes, 클러스터 정보를 저장할 nodes.conf, 노드 연결 타임아웃을 지정하는 cluster-node-timeout, 그리고 AOF(Append Only File) 기능 활성화 같은 항목들이 포함되어 있습니다.
또한 레디스가 과도하게 메모리를 사용할 경우를 대비해 maxmemory를 2GB로 제한하고, 메모리가 부족할 때는 LRU(Least Recently Used) 알고리즘으로 가장 오래된 데이터를 우선 제거하도록 allkeys-lru 정책을 설정했습니다.
# dev-redis-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dev-redis-token-config
namespace: dev-db
data:
redis.conf: | # Redis 설정 파일 (multiline literal)
maxmemory 2gb # 최대 메모리 사용량 제한
maxmemory-policy allkeys-lru # 메모리 초과 시 LRU 정책으로 모든 키 대상 제거
cluster-enabled yes # Redis 클러스터 모드 활성화
cluster-config-file nodes.conf # 클러스터 정보 저장 파일
cluster-node-timeout 5000 # 노드 연결 타임아웃(ms)
appendonly yes # AOF(Append Only File) 방식 활성화
dir /data # 데이터 및 설정 파일 저장 디렉토리 (PVC로 마운트됨)
3) 클러스터 초기화 Job
레디스 클러스터는 단순히 여러 인스턴스를 띄운다고 해서 자동으로 구성되지는 않습니다. 각각의 레디스 인스턴스가 모두 실행된 뒤, redis-cli --cluster create 명령을 통해 클러스터로 묶어주는 과정이 필요합니다.
이 작업을 자동으로 처리하기 위해 쿠버네티스의 Job 리소스를 활용하였습니다. redis:7.0-alpine 이미지를 기반으로 간단한 Job을 만들고, 그 안에서 redis-cli 명령을 실행해 레디스 노드들이 서로 연결되도록 구성했습니다.
Job은 ArgoCD와 연동해 PostSync Hook으로 설정해두었고, Redis Pod가 모두 준비된 이후 자동으로 실행됩니다. 클러스터 초기화 과정까지 배포에 포함되므로, GitOps 기반의 운영 방식에 적합한 구성입니다.
#dev-redis-cluster-init-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: redis-cluster-init
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
template:
spec:
containers:
- name: redis-cluster-init
image: redis:7.0-alpine
command: ["redis-cli", "--cluster", "create",
"dev-redis-token-0.dev-redis-token.dev-db.svc.cluster.local:6379",
"dev-redis-token-1.dev-redis-token.dev-db.svc.cluster.local:6379",
"dev-redis-token-2.dev-redis-token.dev-db.svc.cluster.local:6379",
"--cluster-replicas", "0"]
restartPolicy: OnFailure
4) Headless Service 구성
Redis Cluster를 구성하려면 각 노드가 서로를 정확히 인식할 수 있는 고정된 DNS 주소를 가져야 합니다. 이를 위해 Headless Service를 사용합니다.
일반적인 Service는 하나의 IP로 트래픽을 라운드로빈 방식으로 분산하지만, Headless Service는 clusterIP: None으로 설정되어 각 pod의 고유 DNS가 그대로 노출됩니다. 따라서 dev-redis-token-0.dev-redis-token.dev-db.svc.cluster.local처럼 각 레디스 노드에 직접 접근할 수 있어, 클러스터 구성에 꼭 필요한 노드 간 통신이 가능합니다.
또한 레디스 클러스터에서는 두 가지 포트를 함께 열어두는 것이 중요합니다.
#dev-redis-service.yaml
apiVersion: v1
kind: Service
metadata:
name: dev-redis-token
namespace: dev-db
labels:
app: dev-redis-token
spec:
clusterIP: None
ports:
- port: 6379
name: redis
- port: 16379
name: cluster
selector:
app: dev-redis-token
5) Kustomize로 리소스 구성 관리
Redis Cluster 구성은 여러 YAML 파일로 나뉘어 있습니다. 이렇게 리소스가 많아질 경우, kubectl apply 명령으로 각각 적용하기보다 쿠버네티스의 Kustomize 기능을 활용해 묶어서 관리하는 것이 훨씬 효율적입니다.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev-db
resources:
- dev-redis-cluster-init-job.yaml
- dev-redis-configmap.yaml
- dev-redis-service.yaml
- dev-redis-stateful.yaml
쿠버네티스에 Redis Cluster를 구성한 이후, 이제 이를 실제 서비스 코드에서 활용해야 합니다. Spring Boot 애플리케이션에서 Lettuce 기반 클러스터 연결 설정을 통해 레디스와 통신할 수 있도록 설정을 구성했습니다.
먼저 클러스터에 접속하기 위한 레디스 노드 정보는 application.yaml 또는 환경 변수로부터 전달받습니다.
spring:
data:
redis:
cluster:
nodes: ${REDIS_CLUSTER_NODES}
자바 설정 클래스(RedisClusterConfig.java)에서 이 정보를 바탕으로 클러스터 구성을 생성합니다. 설정에는 크게 세 가지 핵심 요소가 포함되어 있습니다
1) RedisClusterConfiguration
2) LettuceClientConfiguration
3) RedisTemplate 설정
전체 코드
package com.playus.userservice.global.config.data.redis;
import io.lettuce.core.SocketOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@Profile({"dev", "prod"})
public class RedisClusterConfig {
@Value("${spring.data.redis.cluster.nodes}")
private String redisClusterNodes;
@Value("${spring.data.redis.cluster.max-redirects:3}") // 기본값 3
private int maxRedirects;
@Value("${spring.data.redis.timeout:3s}") // 기본값 3초
private Duration timeout;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig = createClusterConfiguration();
LettuceClientConfiguration clientConfig = createClientConfiguration();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 직렬화 설정
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.setEnableTransactionSupport(true);
template.afterPropertiesSet();
return template;
}
private RedisClusterConfiguration createClusterConfiguration() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
// 노드 파싱
String[] nodes = redisClusterNodes.split(",");
for (String node : nodes) {
String[] hostPort = node.trim().split(":");
if (hostPort.length == 2) {
clusterConfig.clusterNode(hostPort[0].trim(), Integer.parseInt(hostPort[1].trim()));
}
}
clusterConfig.setMaxRedirects(maxRedirects);
return clusterConfig;
}
private LettuceClientConfiguration createClientConfiguration() {
// K8s 환경 최적화된 소켓 옵션
SocketOptions socketOptions = SocketOptions.builder()
.connectTimeout(Duration.ofSeconds(5)) // K8s 네트워크 지연 고려
.keepAlive(true) // Keep-alive 활성화
.tcpNoDelay(true) // 지연 최소화
.build();
// 클러스터 토폴로지 새로고침 - K8s 환경용
ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh(Duration.ofSeconds(30)) // K8s Pod 재시작 빠른 감지
.enableAllAdaptiveRefreshTriggers() // 장애 시 빠른 감지
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(5))
.build();
// 클러스터 클라이언트 옵션
ClusterClientOptions clientOptions = ClusterClientOptions.builder()
.socketOptions(socketOptions)
.topologyRefreshOptions(topologyRefreshOptions)
.autoReconnect(true) // 자동 재연결
.pingBeforeActivateConnection(true) // 연결 전 상태 확인
.validateClusterNodeMembership(true) // 클러스터 검증 활성화
.build();
return LettuceClientConfiguration.builder()
.clientOptions(clientOptions)
.commandTimeout(timeout)
.shutdownTimeout(Duration.ofSeconds(1)) // 빠른 셧다운
.build();
}
}
🥳 데이터 저장 확인

샤딩(Sharding)이란?
샤딩은 데이터를 여러 서버(노드)에 나눠 저장하는 방식으로, 성능 병목을 줄이고 수평 확장을 가능하게 해줍니다.
Redis Cluster의 샤딩 방식: 해시 슬롯
Node 0: 슬롯 0 ~ 5461 (5,462개)
Node 1: 슬롯 5462 ~ 10922 (5,461개)
Node 2: 슬롯 10923 ~ 16383 (5,461개)
샤딩 동작 과정
데이터 저장
1) SET user:123 "data" 요청
2) CRC16("user:123") % 16384 = 8000번 슬롯 계산
3) 슬롯 8000은 Node 1이 담당
4) Node 1에 데이터 저장
데이터 조회
1) GET user:123 요청
2) 같은 방식으로 슬롯 계산
3) Node 1에서 데이터 조회
4) 결과 반환
샤딩 확인 방법
# 클러스터 슬롯 분배 확인
kubectl exec -it dev-redis-token-0 -n dev-db -- redis-cli cluster slots
# 각 노드별 키 개수 확인
kubectl exec -it dev-redis-token-0 -n dev-db -- redis-cli DBSIZE
kubectl exec -it dev-redis-token-1 -n dev-db -- redis-cli DBSIZE
kubectl exec -it dev-redis-token-2 -n dev-db -- redis-cli DBSIZE

😃 이번 프로젝트에서는 MSA 환경에서 레디스를 안정적으로 띄우기 위해 쿠버네티스에 직접 Redis Cluster를 구성하고, Spring Boot 애플리케이션과 연동하는 과정을 구현해보았습니다.
아직 replica 노드가 없는 단순한 Master-only 구성이고, 장애 시 자동 복구나 모니터링 체계는 부족한 상태입니다. 하지만 직접 클러스터를 구성해보고, 운영 흐름에 통합하는 경험 자체가 흥미로웠습니다.
향후에는 Replica 노드를 추가해 고가용성을 확보하고, 필요하다면 Redis Operator나 자동 장애 복구 시스템까지 도입하는 방향으로 점진적으로 발전시켜보고 싶습니다.