왜 모니터링이 필요한가
캐시를 적용했다고 끝이 아니다. "정말 잘 동작하고 있는가?"를 확인해야 한다.
- 적중률이 10%면? 캐시가 거의 안 되고 있다. TTL이 너무 짧거나, 키 설계가 잘못됐을 수 있다.
- 제거 횟수가 지나치게 많으면?
maximumSize가 너무 작아서 데이터가 들어오자마자 밀려나고 있다. - 특정 캐시만 미스가 많으면? 그 캐시의 설정을 개별 튜닝해야 한다.
이런 판단을 하려면 숫자로 된 통계가 있어야 한다. Caffeine의 recordStats와 Spring Actuator를 연동하면, 캐시의 적중/미스/제거 횟수를 API로 조회할 수 있다.
Actuator가 노출하는 캐시 관련 엔드포인트와 메트릭을 한눈에 정리하면 이렇다.
설정
recordStats 활성화
Caffeine이 통계를 수집하려면 recordStats를 켜야 한다. 꺼져 있으면 Actuator에서 캐시 메트릭이 안 나온다.
application.yml로 설정하는 경우:
spring:
cache:
type: caffeine
cache-names:
- users
- channels
- notifications
caffeine:
spec: >
maximumSize=100,
expireAfterWrite=600s,
recordStats
Java Config로 설정하는 경우:
Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(600, TimeUnit.SECONDS)
.recordStats() // 이 한 줄 추가
.build();
통계 수집은 약간의 오버헤드를 추가한다. 극도의 성능이 필요한 환경에서는 운영 서버에서 끄고 모니터링 서버에서만 켜는 경우도 있다. 하지만 대부분의 애플리케이션에서 체감할 수 있는 차이는 없다.
Actuator 엔드포인트 노출
Actuator의 캐시 관련 엔드포인트를 노출해야 한다.
management:
endpoints:
web:
exposure:
include: health,caches,metrics
/actuator/caches: 등록된 캐시 목록/actuator/metrics/cache.gets: 캐시 조회 통계 (히트/미스)/actuator/metrics/cache.evictions: 캐시 제거 통계/actuator/metrics/cache.size: 현재 캐시 항목 수
캐시 목록 조회
curl http://localhost:8080/actuator/caches
{
"cacheManagers": {
"cacheManager": {
"caches": {
"users": { "target": "com.github.benmanes.caffeine.cache.BoundedLocalCache" },
"channels": { "target": "com.github.benmanes.caffeine.cache.BoundedLocalCache" },
"notifications": { "target": "com.github.benmanes.caffeine.cache.BoundedLocalCache" }
}
}
}
}
등록된 캐시 이름과 구현체를 확인할 수 있다. @Cacheable("users")로 사용한 캐시가 실제로 등록되어 있는지 확인하는 용도다.
적중률 분석
가장 중요한 메트릭이다. 캐시 히트와 미스 횟수를 조회한다.
curl http://localhost:8080/actuator/metrics/cache.gets?tag=cache:users
{
"name": "cache.gets",
"measurements": [
{ "statistic": "COUNT", "value": 150.0 }
],
"availableTags": [
{ "tag": "result", "values": ["hit", "miss"] },
{ "tag": "cache", "values": ["users", "channels", "notifications"] }
]
}
히트와 미스를 각각 조회하려면 result 태그를 추가한다.
# 캐시 히트 횟수
curl "http://localhost:8080/actuator/metrics/cache.gets?tag=cache:users&tag=result:hit"
# 캐시 미스 횟수
curl "http://localhost:8080/actuator/metrics/cache.gets?tag=cache:users&tag=result:miss"
적중률은 이렇게 계산한다.
적중률 = hit / (hit + miss)
| 적중률 | 의미 | 조치 |
|---|---|---|
| 90% 이상 | 우수 | 유지 |
| 70~90% | 양호 | TTL 또는 maximumSize 조정 검토 |
| 50% 이하 | 문제 | 키 설계, TTL, 무효화 로직 전면 점검 |
서버가 막 시작되면 캐시가 비어있어서 모든 요청이 미스다. 시간이 지나면서 캐시가 채워지면 적중률이 올라간다. 워밍업 후의 적중률로 판단해야 한다.
제거 통계
캐시에서 항목이 제거된 횟수를 확인한다.
curl http://localhost:8080/actuator/metrics/cache.evictions?tag=cache:users
{
"name": "cache.evictions",
"measurements": [
{ "statistic": "COUNT", "value": 25.0 }
]
}
제거가 빈번하면 maximumSize가 너무 작은 것이다. 데이터가 들어오자마자 밀려나고, 밀려난 데이터가 다시 요청되면 DB를 조회해야 한다. maximumSize를 늘리면 적중률이 올라간다.
캐시 크기
현재 캐시에 저장된 항목 수를 확인한다.
curl http://localhost:8080/actuator/metrics/cache.size?tag=cache:users
{
"name": "cache.size",
"measurements": [
{ "statistic": "VALUE", "value": 42.0 }
]
}
maximumSize=100으로 설정했는데 실제 크기가 항상 100 근처면, 캐시가 가득 차서 제거가 빈번하다는 뜻이다. 크기를 늘려야 한다.
모니터링 흐름 요약
캐시를 적용하고 모니터링하는 전체 흐름을 정리하면 이렇다.
초기 설정 후 Actuator 지표를 주기적으로 확인하고, 지표가 저조하면 원인을 파악해서 튜닝한다. 튜닝 후 다시 지표를 확인하는 반복 사이클이 핵심이다. 원인에 따라 조치가 달라진다.
TTL 조정: 적중률이 낮고 캐시 미스가 많으면 TTL이 너무 짧은 것이다.크기 조정:cache.evictions가 많으면maximumSize를 늘려야 한다.키/무효화 재설계: 히트 수가 극히 낮으면 키가 너무 세분화되었거나,@CacheEvict가 너무 잦아서 저장하자마자 삭제되고 있다.
자주 하는 실수
Actuator에서 캐시 메트릭이 0으로 나오거나 아예 안 보이는 가장 흔한 원인이다. caffeine.spec에 recordStats를 빠뜨렸거나, Java Config에서 .recordStats()를 호출하지 않은 경우다. 설정을 넣었는데도 안 되면, 캐시 매니저가 두 개 등록되어 Actuator가 다른 쪽을 바라보고 있는 건 아닌지 확인하자.
[!BUG] 엔드포인트가 404라면 — exposure 설정 확인
management.endpoints.web.exposure.include에 caches와 metrics가 포함되어야 한다. Spring Boot 기본값은 health만 노출하므로 직접 추가해야 한다. 빠뜨리면 /actuator/metrics/cache.gets 자체가 404를 반환한다.
[!WARNING] 적중률은 워밍업 이후에 판단
서버가 막 뜨면 캐시가 비어있어서 모든 요청이 미스다. 이때 측정한 적중률은 의미가 없다. 충분한 트래픽이 흐른 뒤 (최소 TTL × 2 이상 경과) 측정해야 실제 운영 적중률에 가깝다.
면접 Q&A
1. recordStats + Actuator로 적중률을 확인한다. 적중률이 낮으면 캐시가 제 역할을 못하고 있다.
2. TTL이 너무 짧지 않은지 확인. TTL 1초면 사실상 매번 DB를 조회하는 것과 같다.
3. @CacheEvict가 너무 빈번하게 호출되는지 확인. 쓰기가 읽기보다 많으면 캐시를 적용하기 부적합한 데이터다.
4. 캐시 키가 너무 세밀해서 같은 키 히트가 거의 없는 건 아닌지 확인.
[!QUESTION] 캐시 스탬피드가 뭐고, 어떻게 해결하나요?
TTL이 만료되는 순간 다수의 요청이 동시에 캐시 미스를 만나서 DB에 동일 쿼리가 쏟아지는 현상이다. 해결 방법:
1. LoadingCache : 같은 키에 대해 하나의 스레드만 DB를 조회하고 나머지는 대기
2. refreshAfterWrite : TTL 후에도 기존 값을 반환하면서 백그라운드로 갱신
3. 확률적 조기 갱신 : TTL 만료 전에 일정 확률로 미리 갱신 (고급 기법)
함정 : "TTL을 아주 길게 잡으면 되지 않나요?" → 데이터 신선도가 떨어진다. 스탬피드를 방지하면서도 적절한 TTL을 유지해야 한다.
[!QUESTION] 로컬 캐시(Caffeine)의 한계는?
서버가 여러 대일 때, 서버 A에서 데이터를 수정해도 서버 B의 로컬 캐시에는 반영되지 않는다. 사용자가 로드밸런서를 통해 다른 서버에 요청하면 오래된 데이터를 볼 수 있다. 이 문제를 해결하려면 Redis 같은 분산 캐시를 사용하거나, 캐시 무효화 이벤트를 서버 간에 전파해야 한다.