Cache Invalidation

캐시에 저장된 데이터가 더 이상 유효하지 않을 때 제거하거나 갱신하는 것입니다.

왜 필요한가?

DB 데이터 변경 → 캐시는 그대로 → 캐시와 DB 불일치
                                          ↓
                              Cache Invalidation으로 해결

Invalidation 방법 3가지

1. TTL (Time To Live) - 만료 시간 설정

// 가장 단순한 방법 - 시간이 지나면 자동 삭제
redisTemplate.opsForValue()
    .set("user:1", user, 10, TimeUnit.MINUTES);  // 10분 후 자동 만료

// 장점: 구현 단순
// 단점: TTL 동안 stale 데이터 존재 가능

2. Event-Based Invalidation - 변경 시 즉시 삭제

@Service
public class UserService {

    public void updateUser(Long id, UserUpdateRequest request) {
        // DB 업데이트
        userRepository.update(id, request);

        // 변경 즉시 캐시 삭제
        redisTemplate.delete("user:" + id);  // 다음 조회 시 DB에서 최신값 로드
    }

    // 연관 캐시 전체 삭제
    public void updateUserDept(Long deptId) {
        deptRepository.update(deptId);

        // 해당 부서 관련 캐시 전체 삭제
        Set<String> keys = redisTemplate.keys("dept:" + deptId + "*");
        redisTemplate.delete(keys);
    }
}

3. Spring @CacheEvict - 선언적 캐시 삭제

@Service
public class UserService {

    // 조회 시 캐시 저장
    @Cacheable(value = "users", key = "#id")
    public User getUser(Long id) {
        return userRepository.findById(id).orElseThrow();
    }

    // 업데이트 시 캐시 삭제
    @CacheEvict(value = "users", key = "#id")
    public void updateUser(Long id, UserUpdateRequest request) {
        userRepository.update(id, request);
    }

    // 전체 캐시 삭제
    @CacheEvict(value = "users", allEntries = true)
    public void deleteAll() {
        userRepository.deleteAll();
    }

    // 캐시 업데이트 (삭제 후 재저장)
    @CachePut(value = "users", key = "#id")
    public User updateAndCache(Long id, UserUpdateRequest request) {
        return userRepository.update(id, request);
    }
}

Invalidation 전략 비교

전략시점정합성구현 난이도
TTL시간 만료 시낮음 (stale 허용)O 단순
Event-Based데이터 변경 시 즉시높음보통
@CacheEvict메서드 실행 시높음O 단순

주의 - Cache Stampede

캐시 만료 순간 대량 요청 → 모두 DB로 → DB 과부하

시간 →
캐시 만료!
요청1 → DB 조회 중...
요청2 → DB 조회 중...   ← 동시에 몰림 X
요청3 → DB 조회 중...
// 해결책 1: 뮤텍스 락
public User getUser(Long id) {
    User cached = redisTemplate.opsForValue().get("user:" + id);
    if (cached != null) return cached;

    // 락 획득한 스레드만 DB 조회
    String lockKey = "lock:user:" + id;
    Boolean locked = redisTemplate.opsForValue()
        .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);

    if (locked) {
        User user = userRepository.findById(id).orElseThrow();
        redisTemplate.opsForValue().set("user:" + id, user, 10, TimeUnit.MINUTES);
        redisTemplate.delete(lockKey);
        return user;
    }

    // 락 못 얻은 스레드 → 잠깐 대기 후 캐시 재조회
    Thread.sleep(50);
    return getUser(id);
}

// 해결책 2: TTL에 랜덤값 추가 → 만료 시점 분산
int ttl = 600 + new Random().nextInt(60);  // 600~660초
redisTemplate.opsForValue().set("user:" + id, user, ttl, TimeUnit.SECONDS);

Cache Invalidation은 "언제 캐시를 믿을 수 없게 되는가" 를 관리하는 것입니다. 컴퓨터 과학에서 가장 어려운 문제 중 하나로 꼽히며, 정합성과 성능 사이의 트레이드오프를 항상 고려해야 합니다.

CSLOG에서 보기