분산 캐시 환경에서의 고려 사항

개발자 팀·2026년 1월 4일

self-study-series

목록 보기
1/16

분산 환경이란?

여러 애플리케이션 서버가 하나의 Redis 서버를 공유하는 환경입니다.

서버 A (애플리케이션) ─┐
                        ├──→ Redis 서버
서버 B (애플리케이션) ─┘

분산 캐시 환경을 구축하려면 아래와 같은 고려사항들을 염두하여 구축해야 합니다.

1. 캐시 일관성 보장

문제: 서버 A에서 데이터를 업데이트하고 캐시를 삭제했지만, 서버 B의 캐시는 아직 이전 데이터를 가지고 있을 수 있습니다.

해결 방법:

  1. 캐시 무효화 이벤트 전파
  2. 짧은 TTL 사용: TTL을 짧게 설정하여 자동으로 최신 데이터로 갱신
  3. 버전 관리: 캐시 키에 버전 번호를 포함하여 강제 무효화
// 버전 관리 예시
String cacheKey = "user:profile:v2:" + id; // 버전 포함
// 데이터 업데이트 시 버전 증가 → 자동으로 이전 캐시 무효화

2. 캐시 스탬피드 방지

문제: 캐시가 만료되면 모든 서버에서 동시에 DB 조회가 발생할 수 있습니다.

해결 방법:

  1. TTL 랜덤화
  2. 백그라운드 갱신: TTL 만료 전에 백그라운드에서 미리 갱신
@Scheduled(fixedRate = 300000) // 5분마다 실행
public void refreshPopularData() {
    // 인기 데이터를 미리 갱신하여 만료 방지
    List<Long> popularUserIds = getPopularUserIds();
    popularUserIds.forEach(id -> {
        Optional<User> user = userRepository.findById(id);
        user.ifPresent(u -> redisTemplate.opsForValue().set(
            "user:profile:" + id, u, 3600, TimeUnit.SECONDS
        ));
    });
}

3. Redis 고가용성 (High Availability)

문제: 단일 Redis 서버가 다운되면 모든 캐시가 사라집니다.

해결 방법:

  1. Redis 복제 (Replication): 마스터-슬레이브 구조로 데이터 복제
  2. Redis Sentinel: 자동 장애 조치 (Failover)
  3. Redis Cluster: 데이터를 여러 노드에 분산 저장

Redis 복제 (Replication) 이해하기:

Redis 복제는 하나의 마스터(Master) 서버와 하나 이상의 슬레이브(Slave) 서버로 구성됩니다. 마스터 서버의 모든 데이터 변경사항이 슬레이브 서버에 자동으로 복제됩니다.

복제 구조:

마스터 서버 (Master)
├── 데이터 읽기/쓰기
├── 데이터 변경사항을 슬레이브에 전송
└── 슬레이브 서버들 (Slave)
    ├── 데이터 읽기만 가능
    ├── 마스터로부터 데이터 복제
    └── 마스터 다운 시 승격 가능

복제의 장점:

  1. 읽기 성능 향상: 여러 슬레이브에서 읽기 요청 분산
  2. 데이터 백업: 슬레이브가 자동 백업 역할
  3. 고가용성: 마스터 다운 시 슬레이브 승격 가능

복제 설정 예시:

# 슬레이브 서버 설정 (redis.conf)
replicaof 127.0.0.1 6379  # 마스터 서버 주소
replica-read-only yes     # 읽기 전용 모드

복제 지연 (Replication Lag):

마스터에서 슬레이브로 데이터를 복제하는 데 시간이 걸리므로, 슬레이브는 마스터보다 약간 오래된 데이터를 가질 수 있습니다. 이를 복제 지연이라고 합니다. 일반적으로 밀리초 단위의 지연이 발생하며, 대부분의 경우 문제가 되지 않습니다.

Redis Sentinel 이해하기:

Redis Sentinel은 Redis 서버의 모니터링 및 자동 장애 조치를 담당하는 시스템입니다. 마스터 서버가 다운되면 자동으로 슬레이브를 마스터로 승격시킵니다.

Sentinel 구조:

Sentinel 1 ─┐
Sentinel 2 ─┼──→ 마스터 서버 모니터링
Sentinel 3 ─┘
    │
    ├── 마스터 다운 감지
    ├── 슬레이브 승격
    └── 클라이언트에 새 마스터 주소 알림

Sentinel 설정 예시:

# Redis Sentinel 설정 예시
spring:
    data:
        redis:
            sentinel:
                master: mymaster
                nodes:
                    - localhost:26379
                    - localhost:26380
                    - localhost:26381
                password: redis-password # Master 비밀번호 (설정된 경우)
            timeout: 2000ms
            lettuce:
                pool:
                    max-active: 8
                    max-idle: 8
                    min-idle: 0

Sentinel 모드에서 RedisTemplate 동작 방식:

RedisTemplate을 사용하면 자동으로 전체 Redis 서버에 데이터 조회가 가능합니다. 구체적인 동작 방식은 다음과 같습니다:

  1. 쓰기 작업 (set, delete 등):

    • 자동으로 Master 서버에 라우팅됩니다.
    • Master가 다운되면 Sentinel이 자동으로 Slave를 Master로 승격시키고, RedisTemplate이 자동으로 새로운 Master에 연결합니다.
  2. 읽기 작업 (get 등):

    • 기본적으로 Master에서 읽지만, Lettuce는 읽기 부하 분산을 위해 Slave에서도 읽을 수 있습니다.
    • 여러 Slave가 있으면 읽기 요청이 자동으로 분산됩니다.
  3. 자동 Failover:

    • Master가 다운되면 Sentinel이 자동으로 감지하고, 가장 적합한 Slave를 Master로 승격시킵니다.
    • RedisTemplate은 자동으로 새로운 Master 주소를 받아 재연결합니다.
    • 애플리케이션 코드 변경 없이 자동으로 처리됩니다.

실제 코드 예시:

@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Optional<User> getUserProfile(Long id) {
        String cacheKey = "user:profile:" + id;

        // Sentinel 모드에서는 자동으로:
        // 1. Master/Slave 중 적절한 서버에 연결
        // 2. Master 다운 시 자동으로 새로운 Master에 재연결
        // 3. 코드 변경 없이 자동으로 처리됨
        Object cached = redisTemplate.opsForValue().get(cacheKey);

        if (cached != null) {
            return Optional.of((User) cached);
        }

        // DB 조회 및 캐시 저장
        Optional<User> user = userRepository.findById(id);
        if (user.isPresent()) {
            // 쓰기는 자동으로 Master에 저장됨
            redisTemplate.opsForValue().set(
                cacheKey,
                user.get(),
                3600,
                TimeUnit.SECONDS
            );
        }

        return user;
    }
}

중요한 점:

  • 코드 변경 불필요: 기존 RedisTemplate 코드를 그대로 사용하면 됩니다.
  • 자동 라우팅: Spring Data Redis가 자동으로 Master/Slave를 구분하여 라우팅합니다.
  • 자동 Failover: Master 다운 시 자동으로 새로운 Master에 재연결합니다.
  • 읽기 분산: 여러 Slave가 있으면 읽기 요청이 자동으로 분산됩니다.

Redis Cluster 이해하기:

Redis Cluster는 데이터를 여러 노드에 분산 저장하여 수평 확장을 가능하게 합니다. 각 노드는 전체 데이터의 일부만 저장하며, 자동으로 데이터를 분산합니다.

Cluster 구조:

노드 1 (슬롯 0-5460) ─┐
노드 2 (슬롯 5461-10922) ├──→ 전체 데이터 분산
노드 3 (슬롯 10923-16383) ┘

Cluster의 장점:

  1. 수평 확장: 노드 추가로 용량 확장 가능
  2. 고가용성: 노드 다운 시에도 서비스 지속
  3. 자동 샤딩: 데이터 자동 분산

Cluster 설정 예시:

# Redis Cluster 설정 예시
spring:
    data:
        redis:
            cluster:
                nodes:
                    - localhost:7000 # Master 1
                    - localhost:7001 # Master 2
                    - localhost:7002 # Master 3
                    - localhost:7003 # Slave 1 (Master 1의 복제본)
                    - localhost:7004 # Slave 2 (Master 2의 복제본)
                    - localhost:7005 # Slave 3 (Master 3의 복제본)
                max-redirects: 3 # MOVED/ASK 응답 처리 최대 횟수
                password: redis-password # Cluster 비밀번호 (설정된 경우)
            timeout: 2000ms
            lettuce:
                pool:
                    max-active: 8
                    max-idle: 8
                    min-idle: 0

Cluster 모드에서 RedisTemplate 동작 방식:

RedisTemplate을 사용하면 자동으로 전체 클러스터에 데이터 조회가 가능합니다. 구체적인 동작 방식은 다음과 같습니다:

  1. 키 기반 자동 라우팅:

    • 각 키는 CRC16 해시 함수를 통해 0-16383 사이의 슬롯 번호로 변환됩니다.
    • 슬롯 번호에 따라 적절한 Master 노드가 결정됩니다.
    • RedisTemplate이 자동으로 해당 노드에 연결하여 데이터를 저장/조회합니다.
  2. 자동 클러스터 토폴로지 발견:

    • 최소 1개 노드만 알려주면 자동으로 전체 클러스터 구조를 파악합니다.
    • 각 노드의 슬롯 정보를 수집하여 키-노드 매핑을 생성합니다.
  3. MOVED/ASK 응답 자동 처리:

    • 클러스터 재구성(노드 추가/제거) 시 키가 다른 노드로 이동할 수 있습니다.
    • 이 경우 Redis는 MOVED 또는 ASK 응답을 반환합니다.
    • RedisTemplate이 자동으로 적절한 노드로 리다이렉트하여 재시도합니다.
  4. 자동 Failover:

    • Master 노드가 다운되면 해당 노드의 Slave가 자동으로 승격됩니다.
    • RedisTemplate이 자동으로 새로운 Master에 연결합니다.

실제 코드 예시:

@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public Optional<User> getUserProfile(Long id) {
        String cacheKey = "user:profile:" + id;

        // Cluster 모드에서는 자동으로:
        // 1. 키의 해시 값을 계산하여 적절한 노드 결정
        // 2. 해당 노드에 연결하여 데이터 조회
        // 3. 노드 다운 시 자동으로 Slave 승격 및 재연결
        // 4. MOVED/ASK 응답 자동 처리
        // 코드 변경 없이 자동으로 처리됨
        Object cached = redisTemplate.opsForValue().get(cacheKey);

        if (cached != null) {
            return Optional.of((User) cached);
        }

        // DB 조회 및 캐시 저장
        Optional<User> user = userRepository.findById(id);
        if (user.isPresent()) {
            // 키에 따라 자동으로 적절한 노드에 저장됨
            redisTemplate.opsForValue().set(
                cacheKey,
                user.get(),
                3600,
                TimeUnit.SECONDS
            );
        }

        return user;
    }
}

키 분산 예시:

키: "user:profile:1"
  → CRC16 해시: 1234
  → 슬롯: 1234
  → 노드: Master 1 (슬롯 0-5460 담당)

키: "user:profile:2"
  → CRC16 해시: 8000
  → 슬롯: 8000
  → 노드: Master 2 (슬롯 5461-10922 담당)

키: "user:profile:3"
  → CRC16 해시: 15000
  → 슬롯: 15000
  → 노드: Master 3 (슬롯 10923-16383 담당)

중요한 점:

  • 코드 변경 불필요: 기존 RedisTemplate 코드를 그대로 사용하면 됩니다.
  • 자동 샤딩: 키에 따라 자동으로 적절한 노드에 저장/조회됩니다.
  • 전체 클러스터 조회 가능: 모든 노드의 데이터를 조회할 수 있습니다 (키에 따라 자동 라우팅).
  • 자동 Failover: 노드 다운 시 자동으로 Slave 승격 및 재연결됩니다.
  • MOVED/ASK 자동 처리: 클러스터 재구성 시 자동으로 적절한 노드로 리다이렉트됩니다.

고가용성 전략 선택 가이드:

상황권장 방식이유
단일 서버, 읽기 성능 향상복제 (Replication)구현 간단, 읽기 분산
자동 장애 조치 필요Sentinel자동 Failover
대용량 데이터, 수평 확장Cluster데이터 분산, 확장성
소규모 서비스단일 서버 + 백업간단한 구조

주의사항:

캐시 용도로 Redis를 사용하는 경우, 데이터 손실이 허용되므로 복제나 Sentinel만으로도 충분할 수 있습니다. 하지만 대용량 트래픽을 처리해야 하는 경우 Cluster를 고려해야 합니다.

profile
공부하고 기록하고 공유하는 개발자 팀(Tim) 입니다. 늘끄적입니다.

0개의 댓글