레디스 캐싱 전략

캐싱(Cache)?

애플리케이션 성능을 개선하기 위한 핵심 기술로, 자주 사용되거나 최근에 사용된 데이터를 저장해두었다가 재사용하는 기술. 자주 사용되는 데이터를 빠르게 액세스할 수 있는 저장소(메모리, 디스크 등)에 저장하여 읽기 속도를 높이고 시스템 부하를 줄인다. 데이터베이스, API 호출, 파일 시스템, 계산 결과 등 느리거나 비용이 큰 작업의 결과를 캐싱하면 성능이 크게 향상된다.

 

주요 장점

 

  • 속도 향상: 데이터를 메모리 등 빠른 저장소에서 바로 제공해 작업을 빠르게 처리.
  • 시스템 부하 감소: 데이터베이스나 외부 API 호출 빈도를 줄여 자원 사용량 감소.
  • 비용 절감: 네트워크 대역폭 및 서버 자원 사용량 감소.

 

작동 순서

일반적으로 캐싱은 1. 조회 요청(클라이언트의 데이터 요청) → 2. 캐시 확인(Cache Hit/Cache Miss) → 3. 데이터 저장(Cache Miss 발생 시 원본 데이터에서 캐시에 저장.) 의 순서로 작동한다.

더보기
  • Cache Hit: 캐시에 데이터가 있으면 반환.
  • Cache Miss: 캐시에 데이터가 없으면 원본 데이터 소스(ex. 데이터베이스)에서 가져와 캐시에 저장.

 

 

캐싱 종류

  • 클라이언트 측 캐싱 - ex. 웹 브라우저 HTTP 캐시, 쿠키, 로컬 스토리지.

장점: 사용자 단에서 빠르게 데이터를 제공. 서버 부하 감소.

단점: 데이터가 클라이언트에 저장되므로 보안 이슈 발생 가능.

  • 서버 측 캐싱 - ex. Redis, Memcached.

장점: 중앙에서 데이터를 관리하므로 데이터 일관성 유지. 다수의 클라이언트 요청에 대응 가능.

단점: 서버 리소스 사용.

  • CDN 캐싱 - ex. Cloudflare, Akamai.

작동 원리: 정적 파일(이미지, CSS, JavaScript)을 지리적으로 가까운 캐시 서버에 저장.

장점: 사용자에게 빠르게 정적 콘텐츠 제공. 서버 대역폭 감소.

 

주요 전략

  • Cache Aside (Lazy Loading)

작동 원리: 데이터가 요청되면 먼저 캐시를 확인. 캐시에 없으면 원본 데이터 소스에서 데이터를 가져와 캐시에 저장.

특징: 데이터는 필요할 때만 캐시에 저장된다. 일반적으로 가장 널리 쓰이는 방식.

단점: 첫 번째 요청은 캐싱되지 않아 느릴 수 있다.

  • Write Through

작동 원리: 데이터가 데이터베이스에 저장될 때 캐시에도 동시에 저장.

특징: 데이터 일관성이 높다. 그러나 쓰기 속도는 느릴 수 있다. 일반적으로 데이터가 자주 읽히고 변경되는 경우 사용한다.

  • Read Through

작동 원리: 애플리케이션이 캐시에 직접 접근하지 않고, 캐시가 데이터 소스를 대행. 캐시에 없으면 자동으로 데이터 소스에서 가져와 캐시에 저장한다.

특징: 캐시 사용이 투명하고, 캐시 관리를 데이터 소스 계층에 통합할 수 있다.

  • Write Back (Write Behind)

작동 원리: 데이터 변경 시 캐시에만 저장하고, 비동기로 데이터베이스에 저장한다.

특징: 쓰기 성능이 뛰어하지만, 데이터 유실 가능성이 있다. 일반저그올 실시간 쓰기 성능이 중요한 경우 사용한다.

 

고려 사항

  • 데이터 일관성: 데이터베이스와 캐시의 데이터가 다를 경우(캐시 스탠피드), 최신 데이터 반환이 캐시를 갱신하거나 무효화하는 로직을 철저히 관리해야 한다.
  • 메모리 관리: 캐시는 메모리 기반이므로 공간이 제한적이기 때문에, LRU(Least Recently Used)와 같은 캐시 정책을 활용해 오래된 데이터를 자동으로 삭제해야 한다.
  • 캐싱 갱신

TTL (Time to Live): 데이터 유효 기간을 설정. 너무 짧으면 캐시 미스 증가, 너무 길면 데이터가 오래되어 신뢰도 저하.

만료 전략: 주기적으로 데이터를 무효화(Invalidate)하거나 갱신(Refresh)하는 로직 구현.

 

문제 / 해결책

  • 캐시 스탬피드(Cache Stampede)

여러 클라이언트가 동시에 캐시 미스 발생 시 원본 소스에 과도한 요청을 보내는 문제.

※ 해결책

잠금(lock) 메커니즘: 첫 요청만 원본 소스에 접근하고, 나머지는 대기.

TTL 갱신 조정: 캐시 만료 전에 갱신.

 

  • 캐시 오염(Cache Pollution)

자주 사용되지 않는 데이터를 캐싱하여 메모리 낭비.

해결책: LRU/LFU 정책으로 메모리 사용 최적화.

 

  • 데이터 일관성

데이터베이스와 캐시가 불일치할 경우 문제가 발생.

해결책: 데이터 변경 시 캐시 무효화(Invalidate).

 

Redis를 이용한 캐싱 전략

레디스 기본 개념

  • In-memory 데이터베이스: 모든 데이터를 메모리에 저장하므로 읽기/쓰기 속도가 매우 빠르다.
  • Key-Value 저장소: 데이터를 키-값 쌍으로 저장한다.
  • 다양한 데이터 구조 지원: 문자열, 리스트, 해시, 집합(Set), 정렬된 집합 등 다양한 자료형을 제공한다.
  • TTL(Time To Live): 각 데이터에 유효기간을 설정할 수 있어 자동으로 삭제되는 캐시를 구현할 수 있다.
  • 퍼시스턴스 옵션: 메모리에 저장된 데이터를 디스크에 주기적으로 저장하여 데이터 유실을 방지할 수 있다.

 

레디스 캐싱 패턴

  • Read-Through Cache: 데이터를 먼저 캐시에서 찾고, 없으면 데이터베이스나 외부 API에서 가져온 후 캐시에 저장.
  • Write-Through Cache: 데이터를 데이터베이스에 저장할 때 캐시에도 동시에 저장. 쓰기 연산 속도는 느리지만, 읽기 요청 시 항상 최신 데이터를 보장한다.
  • Cache-Aside (Lazy Loading): 애플리케이션이 캐시에 직접 접근하고, 데이터가 없을 경우 데이터베이스에서 가져와 캐시에 저장한다. 가장 흔하게 사용하는 패턴이다.

 

사용 예제 - Read-Through Cache

database 접근 시 redis에 저장되어있는 지 확인 후 없으면 DB에 접근하고 redis에 저장하는 방식.

 

  • Redis
const getAccountByRedis = async (account) => {
  const redis = await getRedis();
  const key = `${ACCOUNT_KEY}:${account}`;
  return tryGetValue(await redis.hgetall(key));
};

 

 

 

  • DB 접근
export const findUserByAccount = async (account) => {
  let result = await getAccountByRedis(account);
  if (result) {
    return result;
  }
  const [rows] = await dbPool.query(SQL_QUERIES.FIND_USER_BY_ACCOUNT, [account]);
  result = rows.length > 0 ? toCamelCase(rows[0]) : null;
  if (result != null) {
    await setAccountByRedis(result);
  }
  return result;
};

 

 

 

 

 

'CS' 카테고리의 다른 글

레이스 컨디션 ?  (0) 2024.11.14
컨텍스트 스위칭?  (0) 2024.11.12
프로세스(Process) / 스레드(Thread) ?  (0) 2024.11.08
면접 질문 연습하기(2)  (1) 2024.10.25
TCP 서버를 공부하기 전에...  (2) 2024.10.17