캐시를 쓰는 이유
캐시는 빈번하게 사용하는 데이터를 빠른 저장소에 두어 원본 데이터 조회 비용을 줄이는 기법입니다.
DB 쿼리, 외부 API 호출, 복잡한 연산 결과 등을 캐시에 보관하면 응답 시간을 줄이고 백엔드 시스템의 부하를 낮출 수 있습니다.
캐시는 저장 위치에 따라 크게 두 가지로 나뉩니다.
- 로컬 캐시(Local Cache): 애플리케이션 프로세스(JVM) 내부에 저장
- 분산 캐시(Distributed Cache): 별도의 캐시 서버에 저장하여 여러 애플리케이션이 공유
로컬 캐시 (Local Cache)
정의
애플리케이션 인스턴스가 실행되는 JVM 힙 메모리에 데이터를 저장하는 캐시입니다. 같은 프로세스 안에서만 캐시를 공유하므로 네트워크 호출이 필요 없습니다.
대표적인 구현체
- Caffeine: 자바 진영에서 가장 많이 쓰이는 고성능 인메모리 캐시. Guava Cache의 후속작
- EhCache: 오래된 자바 캐시 라이브러리. 디스크 저장과 분산 옵션도 제공
- ConcurrentHashMap: 가장 단순한 로컬 캐시. TTL/크기 제한이 없어 직접 구현해야 함
- Spring 기본 ConcurrentMapCacheManager: 별도 설정 없이 사용 가능한 기본값
동작 흐름
[Application Server]
│
├─ JVM Heap
│ └─ Local Cache (Caffeine)
│ ├─ key1 → value1
│ └─ key2 → value2
│
요청이 들어오면 JVM 안에서 바로 데이터를 꺼내므로 응답 시간이 수십 나노초 수준입니다.
장점
- 속도: 메모리 직접 접근이라 네트워크 비용이 0입니다
- 운영 단순함: 별도 인프라가 필요 없습니다. 라이브러리만 추가하면 동작합니다
- 인프라 비용 없음: 추가 서버 비용이 들지 않습니다
- 외부 장애에 강함: 캐시 서버가 죽어서 장애가 나는 경우가 없습니다
단점
- 인스턴스 간 일관성 문제: 서버가 여러 대일 때 각 서버가 별도의 캐시를 들고 있습니다. 한 서버에서 갱신해도 다른 서버는 모릅니다
- 메모리 압박: 캐시 크기가 곧 JVM 힙 사용량입니다. GC에 영향을 줍니다
- 재시작 시 캐시 손실: 프로세스가 종료되면 캐시가 모두 사라집니다 (Cold Start)
- 한 서버의 캐시 크기 한계: 단일 서버 메모리를 초과할 수 없습니다
분산 캐시 (Distributed Cache)
정의
별도의 캐시 전용 서버(또는 클러스터)에 데이터를 저장하고, 여러 애플리케이션 인스턴스가 네트워크를 통해 공유하는 캐시입니다.
대표적인 구현체
- Redis: 가장 많이 쓰이는 분산 캐시. 다양한 자료구조(String, Hash, List, Set, Sorted Set), Pub/Sub, 영속화 지원
- Memcached: 단순한 Key-Value 캐시. 가볍고 빠르지만 자료구조가 제한적
- Hazelcast: 인메모리 데이터 그리드. 분산 락, 분산 큐 등 다양한 기능 제공
- Amazon ElastiCache: AWS 관리형 Redis/Memcached
- Infinispan: Red Hat 진영의 분산 캐시
동작 흐름
[Server A] ─┐
[Server B] ─┼──→ [Network] ──→ [Redis Cluster]
[Server C] ─┘ ├─ key1 → value1
└─ key2 → value2
→ 모든 서버가 같은 데이터를 보게 됨
캐시 조회마다 네트워크 호출이 발생하므로 응답 시간은 밀리초 단위입니다(로컬 캐시보다 1000배 이상 느림). 하지만 DB 조회보다는 훨씬 빠릅니다.
장점
- 인스턴스 간 일관성: 모든 서버가 같은 캐시를 봅니다. 한 곳에서 갱신하면 모두 반영됩니다
- 확장성: 캐시 클러스터를 늘려서 큰 데이터셋도 저장 가능합니다
- 재시작 영향 없음: 애플리케이션이 재시작되어도 캐시는 그대로 유지됩니다
- 데이터 영속화 가능: Redis의 RDB/AOF로 디스크 백업 가능합니다
- 캐시 외 용도: 분산 락, 세션 저장, 메시지 큐 등으로도 활용됩니다
단점
- 네트워크 비용: 모든 캐시 접근이 네트워크 호출입니다. 로컬보다 1000배 이상 느립니다
- 추가 인프라: 캐시 서버 운영(설치, 모니터링, 백업) 비용이 발생합니다
- 캐시 서버 장애 시 영향: 캐시 서버가 죽으면 모든 애플리케이션이 영향을 받습니다 (Cache Stampede)
- 직렬화 비용: 객체를 네트워크로 보내려면 직렬화/역직렬화가 필요합니다
- 네트워크 파티션: 캐시 서버와 통신이 끊기면 응답 지연이 발생합니다
캐시 서버 장애 시나리오
분산 캐시는 한 점의 장애가 시스템 전체에 영향을 줍니다. 그래서 운영 시 다음을 고려해야 합니다.
- 클러스터 구성 (Redis Sentinel, Cluster Mode)
- 타임아웃 설정 (네트워크 hang 방지)
- Circuit Breaker 패턴 (캐시 장애 시 DB 직접 조회로 우회)
- Cache Stampede 방어 (캐시 만료 시 다수 요청이 DB로 몰리는 현상)
실무 선택 기준
로컬 캐시가 적합한 경우
-
변경 빈도가 낮고 모든 인스턴스에서 같은 값을 봐도 되는 데이터
- 코드 테이블 (국가 코드, 카테고리 등)
- 설정값, 상수
- 정적 리소스 메타데이터
-
인스턴스 간 약간의 불일치를 허용할 수 있는 데이터
- 인기 게시글 목록 (몇 초 정도 다르게 보여도 무방)
- 통계 데이터 (실시간성이 중요하지 않음)
- 외부 API 응답 (요금 환율, 날씨 등)
-
호출 빈도가 매우 높아 네트워크 비용이 부담스러운 데이터
- 메서드 호출당 수십~수백만 번 접근하는 경우
- 분산 캐시의 네트워크 왕복이 병목인 경우
-
단일 서버 환경이거나 인스턴스 수가 적은 경우
분산 캐시가 적합한 경우
-
인스턴스 간 일관성이 필요한 데이터
- 사용자 세션
- 인증/인가 정보 (토큰, 권한)
- 장바구니, 주문 임시 데이터
-
데이터 크기가 단일 JVM 힙을 넘는 경우
- 대규모 사용자 데이터 캐싱
- 수십 GB 이상의 캐시
-
캐시 자체가 도메인 기능을 담당하는 경우
- Redis의 자료구조를 활용한 랭킹 시스템 (Sorted Set)
- Pub/Sub으로 실시간 알림
- 분산 락
-
애플리케이션 재시작이 잦거나 캐시 손실이 치명적인 경우
- 무중단 배포 환경
- 캐시 워밍업 비용이 큰 경우
함께 사용하는 경우 (계층형 캐시, Tiered Cache)
두 가지를 함께 쓰는 경우도 있습니다.
요청 → 로컬 캐시(Caffeine) 확인
├─ 히트 → 즉시 반환
└─ 미스 → 분산 캐시(Redis) 확인
├─ 히트 → 로컬 캐시에 저장 + 반환
└─ 미스 → DB 조회 → 양쪽 캐시 저장 + 반환
장점
- 자주 쓰는 데이터는 로컬에서 빠르게 처리
- 분산 캐시로 인스턴스 간 일관성 어느 정도 확보
- DB 부하 최소화
단점
- 일관성 보장이 완벽하지 않음 (로컬 캐시 TTL을 짧게 가져가야 함)
- 구현 복잡도 증가
대표적인 라이브러리로 Caffeine + Redis 조합이 자주 쓰입니다.
주의할 점
1. 로컬 캐시를 분산 환경에 그대로 적용
가장 흔한 실수입니다. 개발할 때 단일 서버에서 잘 동작하다가, 운영에서 서버 여러 대로 늘리면 데이터가 서버마다 다르게 보입니다. 이 경우 다음 옵션이 있습니다.
- 분산 캐시로 전환
- 메시지 브로커(Kafka, Redis Pub/Sub)로 캐시 무효화 이벤트 전파
- TTL을 짧게 가져가서 불일치 허용 시간 제한
2. 분산 캐시를 무조건 빠르다고 가정
분산 캐시는 DB보다는 빠르지만 로컬 캐시보다는 훨씬 느립니다. 호출 빈도가 매우 높은 코드에서 분산 캐시를 쓰면 오히려 병목이 됩니다. 마이크로초 수준 성능이 필요하면 로컬 캐시를 고려해야 합니다.
3. 캐시 만료 후 동시 요청(Thundering Herd)
캐시가 만료되는 순간 동시에 다수의 요청이 DB로 몰려 부하가 급증하는 현상입니다. 분산 캐시일수록 영향이 큽니다.
해결책
- 캐시 만료 전 갱신(refresh-ahead)
- 분산 락으로 한 요청만 DB 조회, 나머지는 대기
- Probabilistic Early Expiration
4. 직렬화 포맷 선택 실수
분산 캐시는 직렬화가 필수입니다. 기본 JDK 직렬화는 느리고 호환성이 떨어집니다. 보통 다음을 사용합니다.
- JSON (Jackson): 가독성 좋음, 성능 보통
- Kryo: 빠르고 작음, 호환성 주의
- Protocol Buffers: 스키마 기반, 매우 빠름
5. TTL을 너무 길거나 짧게 설정
- 너무 길면: 데이터가 변경되어도 한참 동안 옛날 값이 반환됨
- 너무 짧으면: 캐시 효과가 적어지고 DB 부하가 거의 줄지 않음
데이터의 변경 빈도와 허용 가능한 불일치 시간을 기준으로 정해야 합니다.