개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › 아키텍처와 운영 › 대용량 트래픽

캐시와 TTL

캐시를 두면 DB 조회가 얼마나 줄고, 언제 다시 몰리나

인기 상품 하나를 30초 동안 계속 조회합니다. 캐시에 값이 있으면 DB에 가지 않고, 없으면 DB에서 읽어 캐시에 넣어 둡니다(Cache-Aside). 값은 TTL이 지나면 사라집니다.

개념 모형 · 동작 방식을 보이는 모형

이 실습에 나오는 개념

캐시
자주 사용하는 데이터나 계산 결과를 빠르게 접근할 수 있는 곳에 미리 저장해 두고 재사용하는 것이다. 원본(주로 데이터베이스)을 매번 조회하지 않아 응답이 빨라지고 원본의 부하가 줄어든다. 대신 원본이 바뀌었을 때 캐시의 값이 오래된 상태로 남는 문제를 관리해야 한다.
Cache-Aside 패턴
애플리케이션이 캐시를 직접 관리하는 가장 일반적인 방식이다. 데이터를 조회할 때 먼저 캐시를 확인하고, 없으면 데이터베이스에서 읽어 캐시에 저장한 뒤 반환한다. 데이터를 변경할 때는 데이터베이스를 갱신하고 해당 캐시를 삭제한다.
TTL(Time To Live)
데이터가 유효한 시간이다. 캐시에 값을 넣을 때 TTL을 지정하면 그 시간이 지난 뒤 자동으로 삭제된다. 원본과 어긋난 값이 캐시에 무기한 남는 것을 막는 기본적인 안전장치다.
캐시 스탬피드
많이 조회되는 캐시 항목이 만료되는 순간, 동시에 들어온 다수의 요청이 모두 캐시에서 값을 찾지 못하고 한꺼번에 데이터베이스를 조회하는 현상이다. 순간적으로 데이터베이스에 부하가 몰려 장애로 이어질 수 있다. 한 요청만 원본을 조회하게 잠그거나 만료 시간을 분산하는 방법으로 완화한다.
Redis
데이터를 메모리에 저장하는 키-값 저장소다. 문자열뿐 아니라 리스트, 해시, 집합, 정렬된 집합 등의 자료구조를 제공한다. 메모리에서 동작해 매우 빠르며 캐시, 세션 저장소, 분산 락, 순위표, 요청 수 제한 등에 쓰인다. 명령을 하나의 스레드에서 차례로 실행한다.
분산 락
여러 서버에서 실행되는 프로세스들 사이에서 한 번에 하나만 특정 작업을 수행하도록 하는 잠금이다. 자바의 synchronized는 한 서버 안에서만 유효하므로, 서버가 여러 대일 때는 모두가 함께 접근하는 저장소(Redis 등)에 "잠금 표시"를 두는 방식으로 구현한다. 잠금을 건 쪽에 문제가 생겨도 풀리도록 만료 시간을 둔다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 0.0초: 캐시에 값이 없습니다(첫 요청). 요청이 DB를 조회하러 갑니다. 결과가 오기까지 1초가 걸립니다.
  2. 1.0초: DB에서 읽은 값을 캐시에 저장했습니다(TTL 10초). 저장되기 전까지 0.0초부터 요청 5건이 모두 캐시에서 값을 찾지 못해 각자 DB를 조회했습니다. 나머지 요청도 조회가 끝나는 대로 같은 값을 다시 저장하므로, TTL은 마지막 저장(1.8초)부터 셉니다.
  3. 11.8초: TTL이 지나 캐시의 값이 사라졌습니다. 이제부터 오는 요청은 캐시에서 값을 찾지 못합니다.
  4. 11.8초: 캐시에 값이 없습니다(만료 직후). 요청이 DB를 조회하러 갑니다. 결과가 오기까지 1초가 걸립니다.
  5. 12.8초: DB에서 읽은 값을 캐시에 저장했습니다(TTL 10초). 저장되기 전까지 11.8초부터 요청 5건이 모두 캐시에서 값을 찾지 못해 각자 DB를 조회했습니다. 나머지 요청도 조회가 끝나는 대로 같은 값을 다시 저장하므로, TTL은 마지막 저장(13.6초)부터 셉니다.
  6. 23.6초: TTL이 지나 캐시의 값이 사라졌습니다. 이제부터 오는 요청은 캐시에서 값을 찾지 못합니다.
  7. 23.6초: 캐시에 값이 없습니다(만료 직후). 요청이 DB를 조회하러 갑니다. 결과가 오기까지 1초가 걸립니다.
  8. 24.6초: DB에서 읽은 값을 캐시에 저장했습니다(TTL 10초). 저장되기 전까지 23.6초부터 요청 5건이 모두 캐시에서 값을 찾지 못해 각자 DB를 조회했습니다. 나머지 요청도 조회가 끝나는 대로 같은 값을 다시 저장하므로, TTL은 마지막 저장(25.4초)부터 셉니다.

실행 결과

코드

public Product getProduct(long id) {
    String key = "product:" + id;
    Product cached = redis.get(key);
    if (cached != null) return cached;          // 캐시 적중

    Product p = productRepository.findById(id); // DB 조회 (1초)
    redis.set(key, p, Duration.ofSeconds(10));  // TTL 10초
    return p;
}

이 모형의 전제

한 개의 키(인기 상품 하나)만 다루고, 요청은 일정한 간격으로 들어온다고 가정했습니다. 캐시 응답 시간은 0.01초로 두었습니다. 캐시 크기가 가득 차서 오래된 키를 밀어내는 제거(eviction) 정책은 여기서 다루지 않습니다. 락을 건 경우 기다리는 요청은 조회가 끝나는 순간 캐시의 값을 받는다고 단순화했습니다. 숫자는 동작 방식을 보이기 위한 모형이며 실제 Redis를 측정한 값이 아닙니다.

앞 실습: 로드밸런서와 스케일 아웃 · 다음 실습: Redis의 한 줄 처리

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요