1. 캐시가 없는 세상은 끔찍하다
여러분이 '배달의민족'이나 '쿠팡' 같은 거대 트래픽 서비스를 만든다고 상상해봤다. 모든 사용자가 앱을 켤 때마다 DB에 "현재 치킨집 리스트 줘!"라고 요청하면 DB는 어떻게 될까요? 초당 10만 건의 요청이 들어오는 순간, DB CPU는 100%를 찍고 장렬히 전사할 것입니다. (Connection Pool 고갈은 덤입니다.)
이때 우리의 구세주가 등장하니, 바로 캐시(Cache)입니다. 캐시는 "자주 쓰는 데이터나 계산이 복잡한 결과를 가까운 곳(메모리)에 임시로 복사해두는 것"입니다. 하드 디스크(HDD/SSD)에서 읽는 것보다 메모리(RAM)에서 읽는 것이 수만 배 빠르기 때문입니다. CPU L1/L2 캐시부터, 웹 브라우저 캐시, CDN, 그리고 백엔드의 Redis까지, 컴퓨터 공학의 역사는 곧 "캐싱의 역사"라고 해도 과언이 아닙니다.
하지만 캐시는 만능이 아닙니다. 잘못 쓰면 데이터 불일치(Inconsistency)가 발생해, 사용자가 "어? 아까 품절이었는데 왜 다시 재고가 있지?" 하고 혼란에 빠질 수 있습니다. 그래서 상황에 맞는 캐싱 전략(Caching Pattern)을 선택하는 것이 중요합니다.
2. 읽기 전략 (Read Strategies)
데이터를 읽을 때 캐시를 어떻게 활용할까요? 가장 대중적인 방법부터 정리해봤다.
2.1. Look Aside (Cache Aside / Lazy Loading) 패턴
가장 많이 쓰이는 국민 패턴입니다. Redis를 쓴다면 90%는 이 방식입니다.
- 앱이 데이터를 찾습니다.
- 먼저 캐시(Redis)를 찌릅니다.
- 있으면(Cache Hit): 바로 가져옵니다. (DB 안 감 -> 빠름!)
- 없으면(Cache Miss): DB에 가서 데이터를 가져옵니다. 그리고 나중에 또 쓸 수 있게 캐시에 저장해둡니다.
- 장점:
- 장애 격리: 캐시가 죽어도(Redis가 뻗어도) 서비스는 돌아갑니다. 단지 DB로 직접 요청이 가서 느려질 뿐입니다.
- 효율성: 실제로 요청된 데이터만 캐시에 저장됩니다.
- 단점:
- Cold Start: 캐시에 아무것도 없는 초기에는 모든 요청이 DB로 가서 느립니다. 이를 막기 위해 Cache Warming(미리 덥혀두기)을 하기도 합니다.
- 정합성: DB가 업데이트되었는데 캐시는 그대로일 수 있습니다. (TTL 설정 필수).
2.2. Read Through 패턴
앱은 캐시만 바라봅니다. 캐시 저장소(Library)가 DB와 연결되어 있어서, 데이터가 없으면 캐시가 알아서 DB에서 가져와서 업데이트하고 앱에 반환합니다. (Spring Cache 추상화 등이 이에 해당).
- 장점: 앱 코드가 깔끔해집니다(데이터 소스 투명성).
- 단점: 캐시가 죽으면 서비스도 같이 멈출 수 있습니다 (SPOF - Single Point of Failure).
3. 쓰기 전략 (Write Strategies)
데이터를 수정할 때 캐시와 DB를 어떻게 동기화할까요?
3.1. Write Back (Write Behind) 패턴
"일단 캐시에 먼저 쓰고, 나중에 DB에 몰아서 쓴다." 쓰기가 엄청나게 빈번한 서비스(예: 로그 수집, 유튜브 조회수 카운팅)에 적합합니다.
- 데이터를 캐시에 저장합니다. (바로 리턴 -> 엄청 빠름)
- 캐시에 모인 데이터를 일정 주기(예: 1분)마다 DB에 배치(Batch) 작업으로 저장합니다.
- 장점: 쓰기 성능 극대화. DB insert 쿼리 횟수를 획기적으로 줄일 수 있습니다.
- 단점: 치명적인 데이터 유실 가능성. DB에 저장되기 전에 캐시 서버가 꺼지거나 재부팅되면 그 사이의 데이터는 영원히 사라집니다.
3.2. Write Through 패턴
"캐시와 DB에 동시에 쓴다." 데이터 일관성이 최우선일 때 사용합니다.
- 앱이 데이터를 캐시에 씁니다.
- 캐시가 즉시 DB에도 씁니다.
- 둘 다 성공하면 클라이언트에게 완료 응답을 줍니다.
- 장점: 데이터 불일치가 없습니다. 캐시는 항상 최신 상태입니다.
- 단점: 쓸 때마다 2번 저장하므로(쓰기 지연) 느립니다.
3.3. Write Around 패턴
모든 데이터가 캐시에 필요하지는 않습니다. "데이터는 DB에만 쓰고, 읽을 때만 캐시에 넣는다." Write Through와 반대입니다. 쓰기 작업은 DB로 바로 가고, 읽기 작업만 Cache Aside 패턴을 따릅니다. 쓰기 성능은 좋지만, 쓴 직후에 읽으면 Cache Miss가 발생하여 DB를 다시 다녀와야 합니다.
4. 캐시의 3대 재앙: Penetration, Breakdown, Avalanche
코드 리뷰에서 "캐시 아키텍처의 문제점"이 나올 때 나오는 필수 질문들입니다.
4.1. Cache Penetration (캐시 관통)
악의적인 사용자가 DB에도 없는 ID(-1, 99999999)로 계속 요청을 보냅니다. 캐시에 없으니 매번 DB까지 가서 확인합니다. 결국 DB 부하가 치솟아 뻗어버립니다.
- 해결: DB에 데이터가 없다는 사실도 캐시에 저장합니다 (
key: -1, value: null, TTL은 짧게 5분). - 해결2: Bloom Filter를 사용해 없는 데이터를 미리 걸러냅니다.
4.2. Cache Breakdown (Hot Key 만료)
특정 인기 게시물(예: 아이유 컴백 기사)의 캐시가 딱 만료되었습니다. 그 찰나의 순간(0.1초)에 수천 명의 사용자가 동시에 요청합니다. 수천 개의 스레드가 동시에 "어? 캐시에 없네? 내가 DB에서 가져와야지!" 하고 DB로 달려듭니다(Thundering Herd Problem). DB는 즉사합니다.
- 해결: Mutex Lock을 사용합니다. "캐시 갱신은 한 번에 한 놈만!" 나머지는 기다리게 합니다.
4.3. Cache Avalanche (눈사태)
수만 개의 캐시 데이터의 만료 시간(TTL)을 똑같이 "1시간"으로 설정했더니, 1시간 뒤에 동시에 다 만료되었습니다. 모든 요청이 DB로 쏟아집니다. 시스템 전체가 눈사태처럼 무너집니다.
- 해결: TTL에 Random 값(예: 60분 + 0~5분)을 더해서 마감 시간을 분산시킵니다. (Jitter).
5. Local Cache vs Global Cache
캐시를 어디에 둬야 할까요?
5.1. Local Cache (In-Memory)
- 예시: Java의
Ehcache,Caffeine,HashMap. - 위치: 웹 애플리케이션 서버 메모리 내부.
- 장점: 네트워크를 안 타니까 속도가 빛의 속도입니다.
- 단점: 서버가 여러 대일 때, 데이터 불일치가 생깁니다. (서버 A는 가격 100원, 서버 B는 가격 200원). 서버 끄면 데이터 날아갑니다.
5.2. Global Cache (Distributed)
- 예시: Redis, Memcached.
- 위치: 별도의 캐시 서버.
- 장점: 모든 서버가 같은 데이터를 봅니다(일관성). 용량을 쉽게 늘릴 수 있습니다(Scale-out).
- 단점: 네트워크를 타야 하므로 Local보다는 느립니다(그래봤자 1ms 미만).
결론: 전역적으로 공유해야 하는 데이터(사용자 세션, 상품 재고)는 Redis에, 변하지 않는 설정 데이터(공통 코드, 카테고리 정보)는 Local Cache에 두는 하이브리드 방식이 가장 좋습니다.
6. 어떤 데이터를 날려야 할까? (Eviction Policies)
캐시 메모리는 비쌉니다(RAM). 꽉 차면 무언가는 지워야 합니다.
- LRU (Least Recently Used): 가장 오랫동안 안 쓴 놈을 지운다. (가장 일반적이고 합리적임).
- LFU (Least Frequently Used): 가장 적게(빈도) 쓴 놈을 지운다. (한 번만 반짝 인기 있었던 데이터를 지울 때 유리).
- FIFO (First In First Out): 먼저 들어온 놈을 먼저 내보낸다.
7. 마무리 - "일단 캐시하자"는 위험하다
무조건 캐시를 쓴다고 좋은 게 아닙니다.
- 데이터의 일관성(Consistency)이 중요한가? (금융 잔고) -> 캐시 신중히 사용 또는 Write Through.
- 속도가 중요한가? (조회수) -> Write Back.
- 재사용성이 높은가? -> 한 번 읽고 다시 안 읽을 거면 캐시 메모리만 낭비하는 꼴입니다.
적절한 만료 시간(TTL) 설정과 Eviction Policy 선택, 그리고 위에서 본 전략들을 조합해야 진정한 성능 최적화가 완성됩니다. 캐시는 마법이 아니라, 정교하게 설계해야 하는 시스템 구성 요소입니다.