[Redis] 캐싱(caching) 설계 전략에 대해 알아보자 (+TTL 설정 주의점)

반응형

레디스를 캐시로 잘 활용하기 위해서는 어떤 캐싱 전략을 적절히 도입하느냐에 따라 다르다.

이 적절히라는 말은 캐싱 전략을 데이터의 특성과 엑세스 패턴을 잘 고려해 적용하냐는 것이다.

어떤 캐싱 전략들이 있는지 한 번 알아보고 자신의 목적에 맞는 캐싱 전략을 선택할 수 있도록 하자.

 

Redis에 대해 알아보고 싶다면 아래 포스팅을 참고하자.

2023.09.11 - [◼ CS 기초 지식/[데이터베이스]] - [Redis] 레디스란? 특징, 활용예시, 비교 정리


읽기 전략

 

Look Aside 전략

1. cache에서 원하는 cache 데이터가 있는지 조회 (Cache Hit)

2. 없다면 DB에서 조회 (Cache Miss)

3. DB에서 조회한 데이터를 cache에 업데이트

 

데이터를 반복적으로 읽는 작업이 많을 때 사용하는 가장 일반적인 전략이다.

@Cacheable(value = "users") // Cache Hit 시 캐시 값 반환
public User getUser(String id) {
    return userRepository.findById(id); // Cache Miss 시 메서드 실행 및 캐시에 저장
}

이 전략은 cache와 DB가 분리되어 있기 때문에 cache에 장애가 발생하더라도 Server에 영향을 주지 않고 DB에서 데이터를 가져올 수 있다.

 

 

Read Through 전략

1. cache에서 원하는 cache 데이터가 있는지 조회 (Cache Hit)

2. 없다면 cache가 DB에서 데이터를 직접 로드해 저장 (Cache Miss)

3. cache에서 데이터 조회

 

캐시에서만 데이터를 읽어오는 전략이다.

Look Aside와 차이점으로는 캐시를 저장하는 주체가 애플리케이션 서버냐 캐시 자체냐 이다.

이 전략의 경우 캐시가 모든 저장 로직을 중앙 집중적으로 관리하기 때문에 장애 발생시 전체 장애로 이어진다.

또한 Cache Miss 경우 캐시에서 DB를 직접 조회해하기 때문에 구현이 복잡할 수 있다.

이 전략을 사용하기 위해서는 cache 데이터를 분산 하는 작업(Replication, Cluster)으로 고가용성을 보장할 필요가 있다.

만약 애플리케이션이 직접 데이터베이스와 캐시를 관리할 필요 없이 캐시 계층이 모든 처리를 담당해도 된다면 이 전략을 고려할 수 있을 것 같다.

 

읽기 전략에서 주의할 점

주의할점은 'Cache Miss' 상황에서 cache로의 커넥션이 많다면 이 많은 커넥션이 DB로 몰릴 수 있다는 점이다.

예를 들어, cache를 새로 도입했거나 재시작 후에는 캐시가 비어있는 상태이기 때문에

이때 갑자기 많은 요청이 들어오면 ‘Cache Miss’로 모든 요청이 DB로 직행한다.

이 때 DB에 급격한 부하가 발생할 수 있고, 응답 시간도 느려질 수 있다.

이 문제를 방지하기 위해 DB에 저장된 데이터를 cache에 미리 추가해주는 cache warming 작업이 필요하다.


 

쓰기 전략

 

Write Back 전략

1. 모든 데이터를 cache에 저장한다.

2. 캐시 서버에서 일정 시간 마다 배치 처리로 cache에서 데이터를 가져와 DB에 저장한다.

 

일정 시간 마다 배치 처리로 데이터를 한번에 DB에 저장하기 때문에 쓰기 작업이 많은 경우 유용하게 사용할 수 있다.

DB에 데이터를 저장하기 전 cache가 데이터를 모아 놓고 한번에 처리하는 일종의 Queue 역할을 한다고 볼 수 있다.

(ex: 쓰기 작업이 빈번한 로그를 DB에 저장하는 경우 이 전략을 많이 쓴다고 함.)

하지만 In-Memory(인 메모리)이기 때문에 재시작 또는 장애 발생 시 데이터 유실 위험이 있다는 점을 주의해야한다.

RDB, AOF 또는 데이터 분산으로 이 유실 문제를 해결 할 순 있겠지만

현재 구조가 이 방법을 선택하기 적절할지에 대해서는 트레이드 오프를 잘 고려해 선택해야할 것이다.

 

Write Through 전략

1. cache에 데이터 저장

2. DB에 데이터 저장

 

항상 데이터를 저장할 때 cache와 DB 둘 다 저장한다.

cache는 항상 최신의 정보를 갖는다는 장점(정합성)을 갖지만

저장할 때마다 항상 2번의 통신이 이뤄져야하므로 응답시간에 지연이 있을 수 있다.

따라서 쓰기 성능이 중요한 경우에는 적합하지 않다.

 

Write Around 전략

모든 데이터를 DB에만 저장한다.

Write Through에 비해 한 번의 통신을 하기 때문에 빠르다는 장점이 있지만

만약 저장 후 데이터를 캐시에서 데이터를 읽을 시 캐시에는 데이터가 업데이트 되지 않아 정합성 문제가 발생할 수가 있다.

따라서 이 전략의 경우에는 적절한 읽기 전략을 선택해 Cache Miss 상황에서 정합성을 맞춰줄 필요가 있다.

 

쓰기 전략 주의할점

쓰기 전략에서 주의할점은 바로 데이터 정합성이다.

캐시에 데이터를 저장할 때 TTL을 설정하지 않으면 영구적으로 저장되기 때문에 메모리 낭비가 심해져 TTL 설정이 중요하다.

또한 아래에서 설명하겠지만 얼마나 적절한 TTL을 설정하는가도 중요하다.

TTL의 중요성에 대해 아래에서 설명하겠지만 만약 Write Aroud 전략을 사용하고  캐시 데이터를 저장할 때 10분으로 정했다고 해보자.

Look Aside, Read Through 읽기 전략이든 데이터를 읽을 때 먼저 캐시에서 읽는데 이미 캐시에 데이터가 있다.

근데 10분 사이에 데이터가 변경됐다면? 구 데이터가 반환되게 될 것이다.

이 처럼 데이터 성질에 따라 다르겠지만 정합성이 중요하다면 적절한 쓰기 전략과 TTL 설정이 중요하다.


제거와 변경 전략

캐시 공간의 효율성을 위해 그리고 정합성을 위해 제거와 변경을 하는 작업도 아주 중요하다.

 

TTL 설정

TTL이 설정되어 있지 않다면 클라이언트는 오래된 캐시 데이터를 받아 볼 수 있는 문제점이 있다.

또한 In-Memory라는 특성으로 디스크 보다 용량이 훨씬 적기 때문에 이 메모리 관리도 민감한 부분이다.

따라서 cache를 저장할 때 기본적으로 TTL을 설정하는 것이 중요하다.

 

만약 TTL이 너무 짧다면 어떨까?

대규모 트래픽을 받는 서비스이고 서버들이 나눠져있다면 TTL이 짧을 경우 'Cache Stampede' 현상이 발생할 수 있다.

'Cache Stampede'란 cache가 만료되어 다수의 서버들이 동시에 같은 데이터를 DB에서 조회하는 duplicate read가 발생하고

읽어온 값을 동시에 cache에 저장하는 duplicate write가 발생한다.

이는 불필요한 작업들이 늘어나 응답 시간 증가와 장애까지로 이어질 수 있다.

띠라서 TTL을 너무 짧게 설정하는 것을 주의해야한다.

 

반대로 너무 길다면 어떨까?

만약 주간 랭킹 시스템을 도입했고, TTL을 30일로 설정했다고 하면 1주일 마다 갱신되어야할 데이터가 30일동안 유지되어

적절하지 않은 랭킹을 보여줌과 동시에 너무 많은 데이터를 쌓아두는 상황이 발생할 수 있다.

메모리 관리는 민감한 부분임으로 너무 길게 설정하는 것도 주의할 필요가 있다.

 

따라서, 비즈니스 요구사항에 맞춰 너무 짧지거나 길지 않은 적절한 TTL을 설정하는 것이 필요하다.

 

명시적 제거

아래는 Spring의 CacheManager를 사용한 코드이다.

기존의 데이터가 변경되거나 삭제될 경우 애플리케이션 코드에서 직접 캐시 최신화하거나 할 수 있다.

// 캐시 업데이트
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    return userRepository.save(user);
}

// 캐시 제거
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(String userId) {
    userRepository.deleteById(userId);
}

 

캐시 교체 정책

Redis의 경우 메모리 사용량이 설정한 최대치(maxmemory-policy)에 도달했을 때 캐시를 제거하는 알고리즘을 지정할 수 있다.

정책 종류와 자세한 내용은 아래 공식문서에서 확인할 수 있다.

https://redis.io/docs/latest/develop/reference/eviction/#eviction-policies


 

캐싱하기 적합한 데이터는 뭘까? 

캐시는 디스크에 비해 읽기가 아주 빠르다는 장점이 있지만 한정적인 메모리 공간을 사용하기 때문에 이 메모리 관리가 아주 중요하다.

캐시를 적절하게 제거하는 전략도 아주 중요하지만 어떤 데이터를 쓰느냐도 중요한 것이다.

캐시를 적용하기 적합한 데이터는 다음과 같다.

 

자주 조회되고 변화가 적은 데이터

자주 조회되지도 않는 데이터는 굳이 캐시에 저장할 필요 없이 필요할 때에 디스크에서 가져와도 무방하다.

또 변화가 많은 데이터라면 DB와 캐시의 데이터 정합성을 마추기 위해 주기적으로 갱신을 해줘야하는데 

이 경우 DB와 캐시 둘다 통신이 필요하기 때문에 DB의 부하를 줄여줄 수 있는 캐시의 장점을 살리지 못한다.

 

재계산 비용이 큰 데이터

어떤 데이터를 응답하기 위해 복잡한 로직이 있어 10초가 걸린다고 가정해보자.

항상 사용자가 10초 동안 기다려야할까?

물론 데이터 성질에 따라 그럴 수도 있겠지만 빠른 응답이 중요한 경우 재계산 비용이 높은 데이터를 캐싱하여 빠르게 응답해줄 수 있다.


캐시 공유 시 정합성 문제

여러 서버가 하나의 cache를 공유해 사용할 경우 A 서버가 추가한 캐시를 B 서버가 변경해버릴 수 있다.

이 정합성 문제를 방지하기 위해서 분산 락, Spring CacheManager의 sync 옵션 등을 고려하여

각각의 서버마다 데이터의 충돌을 방지할 수 있도록 설계할 필요가 있다.


출처