//
요즘 개발자를 위한 시스템 설계 수업 · 디렌드라 신하, 테자스 초프라
캐싱은 자주 사용하거나 계산 비용이 많이 드는 데이터를 더 빠르게 접근할 수 있는 위치에 저장하고 관리하는 기술이다. 용어부터 정리한다.
| 용어 | 정의 |
|---|---|
| 캐시(Cache) | 자주 접근하는 데이터나 리소스의 복사본을 임시로 저장하는 공간 |
| 캐시 히트(Cache Hit) | 요청한 데이터가 캐시에 있는 경우 |
| 캐시 미스(Cache Miss) | 요청한 데이터가 캐시에 없는 경우 |
| 캐시 제거(Eviction) | 저장소가 가득 찼을 때 새 데이터를 넣기 위해 기존 데이터를 삭제하는 것 |
분산 캐싱(Distributed Caching) 은 여러 서버나 노드에 자주 사용하는 정보를 메모리에 분산 저장하는 기술이다. 핵심 목표는 명확하다. 디스크 기반 저장 시스템에서 데이터를 읽는 속도 문제를 해결하는 것.
| 일반 캐싱 | 분산 캐싱 | |
|---|---|---|
| 적용 범위 | 단일 기기나 서버 내부 | 여러 기기에 분산, 노드 간 데이터 공유 |
| 아키텍처 | 로컬 메모리에 위치, 동일 기기 앱이 바로 접근 | 여러 캐시 노드로 구성된 네트워크. 각 노드가 로컬 캐시를 가지고 노드 간 교환 |
| 확장성 | 소규모 환경에 적합 | 노드 추가로 수평 확장. 대규모 시스템을 위해 설계됨 |
| 일관성/동기화 | 무효화와 동기화가 단순 | 노드 간 동기화 프로토콜이 필요 |
이 표에서 마지막 행이 분산 캐싱의 진짜 비용이다. 노드를 늘려 용량과 처리량을 얻는 대가로, "어느 노드의 값이 최신인가"라는 문제를 새로 얻는다.
장점은 예상 가능하다. 성능 개선, 확장 용이성, 백엔드 부하 감소, 장애 허용성, 일관된 접근 속도, 비용 절감, 사용자 경험 향상.
한계 목록이 더 중요하다.
이 목록에서 실무에 가장 자주 부딪히는 것은 캐시 무효화다. "언제 캐시를 버려야 하는가"는 캐싱을 도입하는 순간 생기는 새로운 설계 문제이고, TTL로 근사하는 방식은 데이터 신선도와 백엔드 부하를 맞바꾸는 미봉책에 가깝다. 원본 데이터의 변경 이벤트에 맞춰 무효화하는 구조를 만들지 않으면, 캐시는 언젠가 "왜 화면에 옛날 값이 보이는가"라는 버그로 돌아온다.
책은 요구 사항 정의부터 시작해 실제로 분산 캐시를 설계하는 과정을 따라간다.
put(key, value), get(key)메모리는 유한하므로 무엇을 버릴지 결정하는 정책이 필요하다. 책은 두 계열로 분류한다.
삽입 순서 기반 (접근 시간을 고려하지 않음)
접근 기반
| 정책 | 기준 |
|---|---|
| MRU (Most Recently Used) | 가장 최근에 사용한 항목을 제거 |
| LRU (Least Recently Used) | 가장 오랫동안 사용되지 않은 항목을 제거 |
| LFU (Least Frequently Used) | 사용 빈도가 가장 낮은 항목을 제거 |
LRU와 LFU의 차이를 구분해 두면 좋다. LRU는 최근성을, LFU는 총 빈도를 본다. 어제 100번 호출됐지만 오늘 한 번도 안 쓰인 키는 LRU에서는 버려지고 LFU에서는 남는다. 트래픽 패턴이 시간에 따라 급격히 바뀌는 서비스라면 LRU가, 인기 항목이 안정적인 서비스라면 LFU가 유리하다.
MRU가 목록에 있는 것이 의외로 보이는데, 순차 스캔처럼 "한 번 읽은 데이터는 다시 읽지 않는" 접근 패턴에서는 방금 쓴 항목을 버리는 것이 오히려 합리적이다.
LRU는 가장 널리 쓰이는 정책이라 책도 구현을 따라간다. 핵심은 해시맵과 이중 연결 리스트를 결합하는 것이다.
해시맵은 키로 노드를 O(1)에 찾게 해 주고, 이중 연결 리스트는 그 노드를 리스트 맨 앞으로 O(1)에 옮기게 해 준다. 두 구조가 함께 있어야 조회와 갱신 모두 상수 시간이 된다.
동작 시 확인해야 하는 경우의 수는 두 가지다.
설계의 마지막 결정은 캐시를 시스템에 어떻게 배치하느냐다. 책은 두 가지 방안을 제시한다.
방법 1: 애플리케이션 서버와 함께 배치
애플리케이션 프로세스 안이나 같은 호스트에 캐시를 두는 방식이다. 네트워크 홉이 없어 가장 빠르지만, 서버마다 캐시가 따로 존재하므로 서버 수가 늘어날수록 캐시 히트율이 떨어지고 서버 간 데이터가 어긋난다.
방법 2: 애플리케이션 서버와 독립적으로 구현
캐시를 별도 클러스터로 분리하는 방식이다. 모든 애플리케이션 서버가 같은 캐시를 공유하므로 히트율과 일관성이 좋지만, 네트워크 왕복 비용이 붙고 캐시 클러스터 자체를 운영해야 한다.
선택 기준으로 책이 나열하는 항목은 다음과 같다. 성능 요구 사항, 확장성, 자원 공유, 유연성, 의존성 및 독립성, 운영 복잡성, 인프라와 비용, 네트워크 지연 허용성.
실무에서는 둘 중 하나가 아니라 두 계층을 함께 쓰는 경우가 많다. 프로세스 내부에 아주 짧은 TTL의 로컬 캐시(L1)를 두고, 그 뒤에 공유 레디스 클러스터(L2)를 둔다. 극단적으로 뜨거운 키의 트래픽은 L1에서 흡수해 레디스로 가는 요청 폭주를 막고, 나머지는 L2에서 일관성을 확보한다. 다만 계층이 늘어나면 무효화해야 할 지점도 늘어난다는 점을 잊지 않아야 한다.
마지막으로 앞서 정의한 요구 사항을 다시 꺼내 설계가 그것들을 만족하는지 점검한다. 이 "요구 사항 검토" 단계는 3부의 설계 실전 장들에서도 매번 반복되는 절차다.
가장 널리 쓰이는 두 가지를 소개한다.
| 레디스(Redis) | 맴캐시드(Memcached) | |
|---|---|---|
| 데이터 구조 | 문자열, 리스트, 집합, 해시 등 다양 | 단순 키-값 |
| 고급 기능 | 영속화 옵션, 발행/구독, 트랜잭션, Lua 스크립팅 | 없음 |
| 용도 | 캐시, 메시지 브로커, 데이터베이스로도 활용 | 순수 메모리 캐싱 |
레디스는 캐시를 넘어 다양한 역할을 소화할 수 있고, 맴캐시드는 단순하고 직관적인 키-값 캐시에 집중한다. 4장에서 본 확률적 자료구조 중 하이퍼로그로그가 레디스에 내장되어 있다는 점도 이 유연성의 사례다.
이 장은 캐싱의 정의에서 시작해 분산 캐싱의 세부 사항, 설계 절차, 대표 솔루션까지 이어진다. 다루는 순서가 깔끔해서 캐싱이라는 주제를 처음 정리하기에는 좋다.
가장 실용적인 대목은 삭제 정책의 분류와 캐시 배치 전략 두 가지였다. 삭제 정책은 "무엇을 버릴지"를 결정하는 문제이고, 배치 전략은 "누가 그 캐시를 공유할지"를 결정하는 문제다. 두 결정이 캐시 히트율과 일관성 특성을 사실상 다 정한다.
반면 분산 캐시 자체의 일관성 유지 메커니즘, 즉 노드 간 동기화 프로토콜은 한계 목록에 이름만 올라가고 구체적으로 다뤄지지 않는다. 실제로 분산 캐시를 운영할 때 가장 골치 아픈 부분이 그쪽이라는 점을 감안하면 아쉬운 지점이다.
이 글이 도움이 되셨나요?