Redis 클러스터 도입기 — WSL2, 클러스터 초기화, 그리고 전체 컨테이너화까지

정영범·2026년 4월 24일

토이프로젝트

목록 보기
6/11

Redis 클러스터 도입기 — WSL2, 클러스터 초기화, 그리고 전체 컨테이너화까지

왜 이 글을 쓰는가

Redis 단일 인스턴스의 한계를 인식하고 클러스터로 전환하기로 했다. 그런데 막상 진행하다 보니 Redis 설정 외에도 개발 환경 자체를 뜯어고치게 됐다. 이 글은 그 과정을 순서대로 정리한 기록이다.


1단계 — WSL2로 개발 환경 이전

처음엔 Windows에서 직접 개발하고 있었다. Docker Desktop을 쓰고 IntelliJ로 실행하는 방식이었다. Redis Cluster를 도입하면서 문제가 생겼다.

Redis Cluster는 클라이언트에게 MOVED 응답을 줄 때 노드의 hostname을 알려준다. Docker 컨테이너 안에서 redis-node-1, redis-node-2 같은 컨테이너 이름으로 hostname을 광고하면, Docker 네트워크 밖의 JVM은 이 이름을 DNS에서 찾지 못한다.

MOVED 5461 redis-node-2:7002
→ Windows JVM: redis-node-2 해석 불가 → 연결 실패

이 문제를 해결하려고 redis-cluster.local이라는 단일 hostname으로 모든 노드를 광고하고, Windows hosts 파일에 127.0.0.1 redis-cluster.local을 추가하는 방식을 썼다. 동작은 했지만 설정이 번거로웠다. Windows hosts 파일은 관리자 권한이 필요하고, 팀원이 생기면 각자 설정해야 한다.

그래서 WSL2(Ubuntu)로 개발 환경을 이전하기로 했다. Linux에서 개발하면 Docker 컨테이너와 같은 네트워크 환경에서 작업할 수 있고, hosts 파일 수정 같은 번거로움도 줄어든다.

# 프로젝트 경로
/home/jybeomss/sideProject

IntelliJ에서 WSL2의 프로젝트를 직접 열어서 개발한다. Windows에서 WSL2 파일시스템에 접근하는 것보다 훨씬 빠르고 안정적이다.


2단계 — Redis Cluster 구성

6노드 구조

3마스터 + 3레플리카 구조로 구성했다.

redis-node-1 (Master) ↔ redis-node-4 (Replica)  슬롯: 0 ~ 5460
redis-node-2 (Master) ↔ redis-node-5 (Replica)  슬롯: 5461 ~ 10922
redis-node-3 (Master) ↔ redis-node-6 (Replica)  슬롯: 10923 ~ 16383

마스터 1개가 죽으면 해당 레플리카가 자동으로 마스터로 승격된다. 마스터 3개 중 2개 이상이 살아있으면 클러스터가 정상 동작한다. 즉 마스터 1개까지의 장애를 허용한다.

docker-compose-dev.yml 핵심 설정

redis-node-1:
  image: redis:7-alpine
  command: >
    redis-server
    --bind 0.0.0.0
    --protected-mode no
    --port 7001
    --cluster-enabled yes
    --cluster-config-file /data/nodes.conf
    --cluster-node-timeout 5000
    --appendonly yes
    --cluster-announce-hostname redis-cluster.local
    --cluster-preferred-endpoint-type hostname
    --cluster-announce-port 7001
    --cluster-announce-bus-port 17001
  ports:
    - "7001:7001"
    - "17001:17001"
  extra_hosts:
    - "redis-cluster.local:host-gateway"
  volumes:
    - redis-node-1-data:/data

각 옵션이 필요한 이유를 짚어보자.

--bind 0.0.0.0
컨테이너 내부 루프백만이 아니라 모든 인터페이스에서 요청을 받는다. Docker 네트워크 외부에서 포트 포워딩으로 접근할 수 있게 된다.

--protected-mode no
Redis는 기본적으로 외부 접속을 막는다. 로컬 개발 환경에서 Docker를 통해 외부 연결을 받아야 하므로 껐다.

--cluster-announce-hostname redis-cluster.local
모든 노드가 동일한 hostname을 광고한다. MOVED 응답을 받은 클라이언트는 항상 redis-cluster.local로 접속을 시도하고, 포트 번호로 각 노드를 구분한다.

--appendonly yes
Redis 재시작 시 데이터가 남도록 AOF(Append Only File) 방식으로 영속화한다.

extra_hosts: - "redis-cluster.local:host-gateway"
컨테이너 내부에서도 redis-cluster.local을 해석할 수 있어야 한다. 노드끼리 클러스터 통신을 할 때 이 hostname을 사용하기 때문이다. host-gateway는 Docker가 호스트 머신의 게이트웨이 IP를 자동으로 주입한다.

정리하면 이런 흐름이다.

[WSL2 JVM]
redis-cluster.local → /etc/hosts → 127.0.0.1
127.0.0.1:7001 ~ :7006 → 각 Redis 노드 (포트 포워딩)

[Redis 컨테이너 내부]
redis-cluster.local → extra_hosts → host-gateway (호스트 IP)
호스트IP:7001 ~ :7006 → 각 Redis 노드 (포트 포워딩)

Lua 스크립트 — 해시 태그 적용

Redis Cluster에서 Lua 스크립트는 모든 키가 같은 노드에 있어야 실행된다. 키가 서로 다른 노드에 분산되면 이 에러가 난다.

CROSSSLOT Keys in request don't hash to the same slot

reserve.lua에서 사용하는 세 키(stock, hold:<reservationId>, holdCount)가 서로 다른 노드에 배치되면 안 된다. 해시 태그로 해결했다. 키에서 {}로 감싼 부분만 슬롯 계산에 사용된다.

// 변경 전 — 각 키가 서로 다른 슬롯에 배치될 수 있음
private fun stockKey() = "stock:default"
private fun holdCountKey() = "holdCount:default"
private fun holdKey(reservationId: UUID) = "hold:$reservationId"

// 변경 후 — {inventory} 기준으로 같은 슬롯에 배치됨
private fun stockKey() = "{inventory}:stock:default"
private fun holdCountKey() = "{inventory}:holdCount:default"
private fun holdKey(reservationId: UUID) = "{inventory}:hold:$reservationId"

Lua 스크립트 코드 자체는 수정이 없었다. 키 prefix만 바꿨다.

Spring 설정

spring:
  data:
    redis:
      cluster:
        nodes:
          - redis-cluster.local:7001
          - redis-cluster.local:7002
          - redis-cluster.local:7003
          - redis-cluster.local:7004
          - redis-cluster.local:7005
          - redis-cluster.local:7006
        max-redirects: 3

모든 노드를 적는 이유가 있다. Spring이 처음 연결할 때 노드 목록 중 하나에 접속해서 클러스터 토폴로지를 받아온다. 하나만 적었는데 그 노드가 죽어있으면 토폴로지 자체를 받지 못한다. 여러 노드를 적어두면 하나가 죽어있어도 다른 노드에서 받아온다.

max-redirects: 3MOVED 응답 시 최대 재시도 횟수다.


3단계 — 클러스터 초기화 자동화

노드를 띄우는 것만으로는 클러스터가 형성되지 않는다. redis-cli --cluster create 커맨드로 초기화가 필요하다. 이걸 수동으로 매번 실행하기 번거로워서 start-dev.sh로 자동화했다.

#!/bin/bash

echo "🚀 Starting development infrastructure..."

# 기존 컨테이너 정리
docker-compose -f docker-compose-dev.yml down 2>/dev/null

# 컨테이너 시작
docker-compose -f docker-compose-dev.yml up -d

# 서비스 준비 대기 (노드가 완전히 뜰 때까지)
sleep 10

# Redis Cluster 초기화
docker exec redis-node-1 redis-cli --cluster create \
  redis-node-1:7001 \
  redis-node-2:7002 \
  redis-node-3:7003 \
  redis-node-4:7004 \
  redis-node-5:7005 \
  redis-node-6:7006 \
  --cluster-replicas 1 \
  --cluster-yes

--cluster-yes로 확인 프롬프트를 건너뛴다. 스크립트 실행 시 -it 없이 동작하도록 인터랙티브 모드를 제거했다.

클러스터 초기화 확인은 이렇게 한다.

docker exec -it redis-node-1 redis-cli -p 7001 cluster info
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_known_nodes:6
# cluster_size:3

# 해시 태그 키 조회 테스트
docker exec -it redis-node-1 redis-cli -c -p 7001 get "{inventory}:stock:default"

4단계 — 전체 컨테이너화

WSL2로 이전하고 클러스터를 구성하다 보니 한 가지 불편함이 생겼다. 인프라(PostgreSQL, Redis, Kafka)는 Docker로 올리고, 애플리케이션(order-service, payment-service, shipping-service)은 IntelliJ에서 직접 실행하는 방식이었다.

서비스가 3개라 각각 실행해야 하고, 설정 파일도 application.yml(로컬용)과 application-docker.yml(컨테이너용) 두 가지를 관리해야 했다. 서비스 간 통신을 테스트하려면 항상 세 개를 모두 실행해야 했다.

그래서 애플리케이션 서비스도 모두 컨테이너화하기로 했다. 개발 시에도 전체를 Docker로 올려서 하는 방식으로 바꿨다.

Dockerfile (Multi-stage build)

# Build stage
FROM gradle:8-jdk21-alpine AS build

WORKDIR /app
COPY . .

# 해당 모듈만 빌드
RUN gradle :order-service:bootJar --no-daemon

# Runtime stage
FROM eclipse-temurin:21-jre-alpine

WORKDIR /app
COPY --from=build /app/order-service/build/libs/*.jar app.jar

EXPOSE 8081

ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]

Multi-stage build로 최종 이미지에 Gradle, JDK 같은 빌드 도구가 포함되지 않게 했다. eclipse-temurin:21-jre-alpine은 JRE만 포함한 경량 이미지다.

Gradle 멀티모듈 프로젝트라서 루트에서 전체를 복사하고 특정 모듈만 빌드하는 방식을 썼다. gradle :order-service:bootJar로 order-service의 실행 가능한 jar만 만든다.

docker-compose.yml — 전체 서비스 포함

services:
  # 인프라
  postgres: ...
  redis-node-1 ~ redis-node-6: ...
  kafka: ...

  # 애플리케이션
  order-service:
    build:
      context: .
      dockerfile: order-service/Dockerfile
    ports:
      - "8081:8081"
    environment:
      - SPRING_PROFILES_ACTIVE=docker
    depends_on:
      postgres:
        condition: service_healthy
      kafka:
        condition: service_healthy
      redis-node-1:
        condition: service_started
      # ... redis-node-2 ~ 6
    networks:
      - eventful-network
    restart: on-failure

  payment-service:
    build:
      context: .
      dockerfile: payment-service/Dockerfile
    ports:
      - "8082:8082"
    ...

  shipping-service:
    build:
      context: .
      dockerfile: shipping-service/Dockerfile
    ports:
      - "8083:8083"
    ...

depends_on으로 서비스 시작 순서를 제어한다. condition: service_healthy는 헬스체크가 통과될 때까지 기다린다. PostgreSQL과 Kafka는 헬스체크가 설정되어 있어서 이 조건을 쓴다. Redis 노드는 헬스체크가 없어서 service_started로 컨테이너가 시작된 시점만 체크한다.

restart: on-failure는 애플리케이션이 비정상 종료될 때 자동 재시작한다. Redis 클러스터 초기화가 완료되기 전에 애플리케이션이 시작되면 연결 실패로 죽는데, 자동 재시작으로 클러스터가 준비되면 알아서 올라온다.

start-all.sh — 전체 실행 스크립트

#!/bin/bash

# 전체 시스템 시작 (인프라 + 애플리케이션)
docker-compose up -d --build

# Redis Cluster 초기화
sleep 10
docker exec redis-node-1 redis-cli --cluster create \
  redis-node-1:7001 redis-node-2:7002 redis-node-3:7003 \
  redis-node-4:7004 redis-node-5:7005 redis-node-6:7006 \
  --cluster-replicas 1 --cluster-yes

# 헬스체크
curl http://localhost:8081/actuator/health
curl http://localhost:8082/actuator/health
curl http://localhost:8083/actuator/health

개발 워크플로우 변화

변경 전

WSL2에서 docker-compose-dev.yml로 인프라만 올리기
→ IntelliJ에서 order-service, payment-service, shipping-service 각각 실행
→ 서비스 수정 시 IntelliJ에서 재시작

변경 후

WSL2에서 ./start-all.sh 한 번 실행
→ 전체 시스템이 올라옴
→ 서비스 수정 시 docker-compose up -d --build order-service로 재빌드

불편한 점도 있다. IntelliJ 디버거를 붙이려면 remote debug 설정이 필요하다. 코드를 수정할 때마다 재빌드 시간이 있다. 그래도 서비스 간 통신 테스트나 전체 플로우 검증은 훨씬 편해졌다.


정리

이번에 바뀐 것들을 정리하면 이렇다.

항목변경 전변경 후
개발 환경WindowsWSL2 (Ubuntu)
Redis 구성단일 인스턴스6노드 클러스터
클러스터 초기화수동start-dev.sh 자동화
애플리케이션 실행IntelliJ 직접 실행컨테이너화
설정 파일application.yml / application-docker.ymlapplication-docker.yml 단일화

Redis 클러스터 하나 도입하려다가 개발 환경 전체를 개선하게 됐다. 처음엔 귀찮았지만 결과적으로 더 깔끔한 구조가 됐다.

profile
벨로그 좋은것만 드려요

0개의 댓글