면접 대비(8/23)
Database

Redis 캐시 전략 & 분산락 — 면접 대비 정리

Redis 자료구조별 사용 케이스, Cache-Aside/Write-Through 전략, Cache Stampede, Redisson 분산락, RDB/AOF 영속성까지 정리한다.

2026-04-02
9 min read
#Redis#캐시#분산락#Redisson#Cache-Aside#성능최적화

Redis가 빠른 이유

인메모리: 데이터를 RAM에 저장한다. 디스크 I/O가 없어 나노초 단위 응답.

단순한 자료구조: 복잡한 쿼리 파싱이 없다. 명령이 단순하고 빠르다.

Single Thread: 이벤트 루프 기반 단일 스레드. 컨텍스트 스위칭과 락 경쟁이 없다. 단, 하나의 명령이 오래 걸리면 전체가 블로킹된다.


자료구조별 사용 케이스

String

가장 기본. 문자열, 숫자, JSON 직렬화 객체.

SET user:1 '{"id":1,"name":"홍길동"}'  EX 3600
GET user:1
INCR page_view_count  # 원자적 증가

사용 케이스: 세션, 캐시, 카운터, 분산락.

Hash

필드-값 쌍의 맵. 객체를 필드별로 저장.

HSET user:1 name 홍길동 age 30 email test@test.com
HGET user:1 name
HGETALL user:1

사용 케이스: 사용자 프로필, 설정값. String에 JSON 전체를 넣으면 특정 필드만 수정해도 전체를 다시 써야 하는데, Hash는 필드 단위 수정이 가능하다.

List

순서가 있는 문자열 목록.

LPUSH queue:email "job1"   # 왼쪽에 추가
RPOP queue:email            # 오른쪽에서 꺼냄
LRANGE notifications:1 0 9  # 0번~9번 조회

사용 케이스: 메시지 큐, 최근 방문 목록, 알림 피드.

Set

중복 없는 문자열 집합.

SADD online_users user:1 user:2 user:3
SISMEMBER online_users user:1  # 포함 여부
SCARD online_users              # 원소 수
SINTER set1 set2               # 교집합

사용 케이스: 온라인 사용자 목록, 태그, 좋아요 중복 방지.

Sorted Set (ZSet)

점수(score)로 정렬된 집합.

ZADD leaderboard 1500 user:1
ZADD leaderboard 2000 user:2
ZREVRANK leaderboard user:2  # 내림차순 순위 (0-based)
ZREVRANGE leaderboard 0 9 WITHSCORES  # 상위 10명

사용 케이스: 실시간 랭킹, 지연 작업 큐(score=실행 시간), 범위 검색.


캐시 전략

Cache-Aside (Lazy Loading)

읽기:
1. Cache에서 조회
2. 히트 → 반환
3. 미스 → DB 조회 → Cache에 저장 → 반환

쓰기:
1. DB에 쓰기
2. Cache 삭제 (Invalidation)
public User getUser(Long id) {
    String key = "user:" + id;
    User cached = redis.get(key, User.class);
    if (cached != null) return cached;

    User user = userRepository.findById(id).orElseThrow();
    redis.set(key, user, Duration.ofHours(1));
    return user;
}

public void updateUser(User user) {
    userRepository.save(user);
    redis.delete("user:" + user.getId()); // 캐시 삭제
}

장점: 실제로 읽히는 데이터만 캐시됨. 캐시 장애 시 DB로 폴백 가능.

단점: 첫 조회 시 항상 Cache Miss. 캐시 삭제와 DB 쓰기 사이 짧은 불일치 가능.

Write-Through

쓰기:
1. Cache에 쓰기
2. DB에 쓰기 (동기)

데이터 일관성이 높지만 쓰기 지연이 발생한다. 캐시에 안 쓰인 데이터는 Cache Miss.

Write-Behind (Write-Back)

쓰기:
1. Cache에 쓰기 (즉시 반환)
2. 비동기로 DB에 쓰기 (배치)

쓰기 성능이 빠르지만 Cache 장애 시 데이터 유실 위험이 있다.


Cache Stampede (캐시 스탬피드)

캐시가 만료된 순간 여러 요청이 동시에 DB로 몰리는 현상.

00:00:00 - 인기 상품 캐시 TTL 만료
00:00:00 - 동시 요청 1000개 → 전부 DB 조회 시도
→ DB 과부하

해결법 1: Mutex Lock (캐시 재생성 락)

public Product getProduct(Long id) {
    String key = "product:" + id;
    Product cached = redis.get(key);
    if (cached != null) return cached;

    // 락 획득 시도 (SET NX EX)
    String lockKey = "lock:product:" + id;
    boolean locked = redis.setIfAbsent(lockKey, "1", Duration.ofSeconds(5));

    if (locked) {
        try {
            Product product = productRepository.findById(id).orElseThrow();
            redis.set(key, product, Duration.ofMinutes(10));
            return product;
        } finally {
            redis.delete(lockKey);
        }
    } else {
        // 락 실패 → 잠깐 대기 후 캐시 재조회
        Thread.sleep(100);
        return redis.get(key);
    }
}

해결법 2: TTL 랜덤 지터

int ttl = 3600 + random.nextInt(600); // 3600~4200초 랜덤
redis.set(key, value, Duration.ofSeconds(ttl));

동일 TTL이면 동시에 만료된다. 지터를 추가하면 만료가 분산된다.

해결법 3: Background Refresh

TTL 만료 전에 비동기로 미리 갱신한다.


분산락 (Distributed Lock)

여러 서버가 있을 때 특정 작업을 하나의 서버만 실행하도록 보장.

SET NX EX (기본 구현)

// 락 획득
Boolean locked = redis.setIfAbsent("lock:order:" + orderId, serverId, Duration.ofSeconds(30));

if (Boolean.TRUE.equals(locked)) {
    try {
        processOrder(orderId);
    } finally {
        // 내가 건 락만 해제 (Lua 스크립트로 원자적 처리)
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                       "return redis.call('del', KEYS[1]) else return 0 end";
        redis.execute(script, List.of("lock:order:" + orderId), serverId);
    }
}

주의: 작업 시간이 TTL보다 길면 락이 먼저 풀려 다른 서버가 진입할 수 있다.

Redisson (실무 권장)

@Autowired
private RedissonClient redisson;

public void processOrder(Long orderId) {
    RLock lock = redisson.getLock("lock:order:" + orderId);

    try {
        boolean acquired = lock.tryLock(5, 30, TimeUnit.SECONDS);
        // tryLock(waitTime=5초, leaseTime=30초, unit)

        if (!acquired) {
            throw new LockAcquisitionException("락 획득 실패");
        }

        doProcessOrder(orderId);

    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

Redisson은 워치독(Watchdog) 기능이 있어, 작업이 진행 중이면 TTL을 자동으로 연장한다.


Redis 영속성

Redis는 기본적으로 인메모리이므로 재시작하면 데이터가 사라진다. 영속성 옵션:

RDB (Redis Database)

특정 시점의 스냅샷을 파일로 저장.

save 900 1    # 900초 내 1번 이상 변경 시 저장
save 300 10   # 300초 내 10번 이상 변경 시 저장

장점: 파일이 작고 복구 속도 빠름.
단점: 마지막 스냅샷 이후 데이터는 유실될 수 있음.

AOF (Append Only File)

모든 쓰기 명령을 로그로 기록.

appendonly yes
appendfsync everysec  # 1초마다 fsync (권장)

장점: 데이터 유실 최소화.
단점: 파일 크기가 크고 복구 속도 느림.

실무: RDB + AOF 혼합 사용. 스냅샷 빠른 복구 + AOF로 유실 최소화.


면접에서 자주 나오는 질문

Q. Redis가 Single Thread인데 빠른 이유는?

데이터가 메모리에 있어 I/O 대기가 없다. 명령이 단순하고 실행이 빠르다. 단일 스레드라 락 경쟁과 컨텍스트 스위칭 비용이 없다. I/O 다중화로 수천 개의 클라이언트 연결을 효율적으로 처리한다.

Q. Cache-Aside에서 캐시 무효화는 언제 하는가?

DB 업데이트 후 캐시를 삭제(delete)한다. 캐시를 업데이트(set)하지 않고 삭제하는 이유는, 동시에 여러 쓰기가 발생할 때 순서가 꼬여 낡은 값이 캐시에 남을 수 있기 때문이다. 삭제 후 다음 읽기 시 DB에서 최신값을 가져온다.

Q. 분산락에서 TTL을 너무 짧게 잡으면 어떤 문제가 생기는가?

작업이 TTL 안에 끝나지 않으면 락이 해제되고, 다른 서버가 같은 작업을 시작한다. 두 서버가 같은 작업을 동시에 처리하게 되어 분산락의 목적이 사라진다. Redisson의 워치독이 이를 해결한다.

Q. Redis 클러스터와 레플리카의 차이는?

레플리카(Replication)는 마스터의 데이터를 슬레이브에 복제한다. 읽기 분산과 장애 시 페일오버에 쓴다. 클러스터(Cluster)는 데이터를 16384개 슬롯으로 샤딩해 여러 노드에 분산 저장한다. 수평 확장이 가능하고 각 노드는 레플리카를 가질 수 있다.