본문 바로가기

Spring Boot/Cache

캐시 모니터링과 문제 해결 : 캐시 성능 분석

 - 캐시 성능을 정량적으로 분석하는 주요 지표

 - 적중률(Hit Ratio)를 계산하고 해석하는 법

 - 캐시가 응답 시간에 미치는 영향을 비교 및 분석

 - 메모리 샤용량과 병목 현상을 모니터링하는 법

 - 실무에서 캐시 성능을 점검하고 개선 방향 수립하기

 


캐시 성능 분석 개요

캐시는 응답속도를 향상시키기 위한 핵심 구성 요소이지만, 무조건 캐시를 사용한다고 해서 성능이 개선되는건 아님

효율적 캐시 설계를 위해선 측정 -> 분석 -> 개선의 반복적 검증 과정이 필요함

 

 

1. 캐시 성능 지표의 중요성

Hit Ratio(캐시 적중률) 캐시에서 데이터를 성공적으로 반환한 비율
Miss Ratio(캐시 미적중률) 캐시가 데이터를 찾지 못해 원본 데이터 소스를 조회한 비율
Eviction Count(캐시 제거 횟수) 용량 초과 또는 만료로 제거된 항목의 수
Load Time(데이터 로딩 시간) 캐시 미적중시 데이터를 새로 불러오는 데 걸린 시간
Memory Usage(메모리 사용량) 캐시 데이터가 차지하는 메모리의 크기

 

 


캐시 적중률(Hit Ratio) 측정

1. 개념

캐시 적중률은 캐시의 성능을 가장 직관적으로 보여주는 지표라고 볼 수 있음

 

Hit Ratio = Cache Hit / (Cache Hit + Cache Miss)

 

 

2. Spring Actuator를 통한 조회 예시

Actuator의 /actuator/metrics/cache.gets 엔드포인트를 사용하면 데이터를 얻을 수 있다

{
    "name": "cache.gets",
    "description": "The number of times cache lookup methods have returned a cached (hit) or uncached (newly loaded or null) value (miss).",
    "measurements": [
        {
            "statistic": "COUNT",
            "value": 0.0
        }
    ],
    "availableTags": [
        {
            "tag": "result",
            "values": [
                "hit",
                "miss"
            ]
        },
        {
            "tag": "cache.manager",
            "values": [
                "caffeine"
            ]
        },
        {
            "tag": "cache",
            "values": [
                "productCache",
                "userCache"
            ]
        },
        {
            "tag": "name",
            "values": [
                "productCache",
                "userCache"
            ]
        }
    ]

 

여기에서 데이터를 얻을 수 있다고 함

나는 캐싱한게 없으니 나오질 않는다..

 

Hit / Miss 값 계산 예시

GET /actuator/metrics/cache.gets?tag=result:hit
GET /actuator/metrics/cache.gets?tag=result:miss

 

이걸 postman이나 브라우저에 입력해주면 값이 나온다

 

가령 hit가 4200, miss가 800이라고 하면 Hit Ratio = (4200 / 5000) * 100 = 84%가 된다

 

 

3. 기준값 설정

구분 Hit Ratio 판단 기준
우수 90% 이상 캐시가 효과적으로 동작중
보통 70 ~ 89% 캐시 정책 개선 필요
낮음 70% 미만 캐시 구조 또는 TTL 재설계 필요

 

 


응답 시간 분석

1. 캐시 사용 전후 비교 실험(?)

 

Product.java

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
public class Product {
    private Long id;
    private String name;
    private int price;
}

 

ProductService.java

import com.b1uffer.actuatortest.entity.Product;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ProductService {

    @Cacheable("productCache")
    public Product getProduct(Long id) {
        simulateSlowService();
        return new Product(id, "노트북", 1500000);
    }

    private void simulateSlowService() {
        try {
            Thread.sleep(1000);
        } catch(InterruptedException e) {

        }
    }
}

 

 

테스트코드

import com.b1uffer.actuatortest.service.ProductService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ActuatorTestApplicationTests {

    @Autowired
    ProductService productService;

    @Test
    void testCacheEffect() {
        long start = System.currentTimeMillis();

        for(int i = 0; i < 3; i++) {
            productService.getProduct(1L);
            System.out.println("duration : " + (System.currentTimeMillis() - start));
        }

        long duration = System.currentTimeMillis() - start;
        System.out.println("Total Time : " + duration + "ms");
    }
}

 

 

결과

2026-01-16T23:34:21.294+09:00  INFO 39752 --- [actuatorTest] [    Test worker] c.b.a.ActuatorTestApplicationTests       : Starting ActuatorTestApplicationTests using Java 17.0.15 with PID 39752 (started by index in C:\practice\actuator\actuatorTest)
2026-01-16T23:34:21.295+09:00  INFO 39752 --- [actuatorTest] [    Test worker] c.b.a.ActuatorTestApplicationTests       : No active profile set, falling back to 1 default profile: "default"
2026-01-16T23:34:22.379+09:00  WARN 39752 --- [actuatorTest] [    Test worker] i.m.core.instrument.MeterRegistry        : This Gauge has been already registered (MeterId{name='custom.cache.size', tags=[]}), the registration will be ignored. Note that subsequent logs will be logged at debug level.
2026-01-16T23:34:23.321+09:00  INFO 39752 --- [actuatorTest] [    Test worker] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 4 endpoints beneath base path '/actuator'
2026-01-16T23:34:23.407+09:00  INFO 39752 --- [actuatorTest] [    Test worker] c.b.a.ActuatorTestApplicationTests       : Started ActuatorTestApplicationTests in 2.389 seconds (process running for 3.344)
duration : 1010
duration : 1012
duration : 1012
Total Time : 1012ms
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
> Task :test
BUILD SUCCESSFUL in 6s
4 actionable tasks: 2 executed, 2 up-to-date
오후 11:34:25: 실행이 완료되었습니다 ':test --tests "com.b1uffer.actuatortest.ActuatorTestApplicationTests.testCacheEffect"'.

 

캐시가 미적용일 때는 DB에 접근하기 때문에 duration이 1000ms 이상 걸린다

이후 캐시가 적용되어 메모리를 조회할 땐 0ms ~ 10ms 정도 걸린다고 한다. 실제로 duration이 2ms 걸렸음

 


메모리 사용량 모니터링

1. JVM Heap 사용 추적

캐시는 메모리에 데이터를 저장하기 때문에 Heap 영역을 지속적으로 점유함

이 때문에 JVM의 메모리 사용량을 함께 모니터링 하는게 중요함

 

나는 윈도우니까 cmd에 이걸 입력해줌

jstat -gcutil <PID> 1s 10

 

 

여기서 <PID>는 spring을 실행한 뒤 

jps -l

 

이걸 cmd에 입력해주면 spring에 대한 PID가 나옴

<PID> com.b1uffer.actuatortest.ActuatorTestApplication

 

뭐.. 이런식으로..

 

그리고 jstat 어쩌고를 입력해주면

  S0     S1     E      O      M     CCS    YGC     YGCT     FGC    FGCT     CGC    CGCT       GCT   
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033
  0.00  52.21  11.11  91.05  98.53  96.12     10     0.030     0     0.000     4     0.004     0.033

 

이런식으로 뜬다

 

* Old (O) 영역과 Metaspace (M)가 일정하게 증가한다면, 캐시 데이터가 과도하게 쌓이고 있을 가능성이 있음

 

 

2. 시각화 도구 활용 (JVisualVM)

1) JVisualVM 실행 - > 애플리케이션 프로세스 선택

2) Heap 탭에서 메모리 사용량 변화를 실시간으로 확인하기

3) GC(Activity) 그래프를 통해 캐시로 인한 GC 빈도 증가 여부 확인하기

 

JVisualVM

 

* Caffeine은 LRU 정책과 만료시간 설정을 통해 메모리 사용량을 자동으로 조절할 수 있음

 


병목 현상 식별

캐시는 일반적으로 성능을 높이지만, 잘못된 설계는 오히려 병목현상을 일으킬 수 있다고 한다

 

1. 대표적 병목 사례

원인  설명 해결법
단일 캐시 키 집중 모든 요청이 동일한 캐시 키를 공유함 캐시 키 분산 구조 설계
TTL 동시 만료 캐시 항목이 한꺼번에 만료되어 DB 부하 증가 만료시간 랜덤화 적용
과도한 캐시 락 여러 스레드가 동시에 캐시 로드를 시도 synchronized block 최소화,
Async Cache 사용

 

 

2. 병목 감지 실험 예시

@Cacheable("heavyCache")
public String loadData(String key) {
	try {
    	Thread.sleep(500);
    } catch(InterruptedException e) {

    }
    return "data for " + key; 
}

 

 

다중 스레드 요청시

ExecutorService executor = Executors.newFixedThreadPool(10);
for(int i = 0; i < 10; i++) {
	executor.submit(() -> service.loadData("sameKey"));
}

 

모든 스레드가 같은 키(sameKey)를 요청하면, 첫번째 스레드가 데이터를 로드하는 동안 나머지는 대기상태(blocking)가 됨

 

* Caffeine의 CacheLoader 또는 비동기 캐시(AsyncCache)를 활용하면 병목 문제를 완화할 수 있음

 


* 캐시 Hit Ratio가 높더라도, 데이터 신선도(Staleness)를 반드시 확인해야함

* 캐시 크기를 늘리는 것은 메모리 부담을 증가시키므로, TTL과 Eviction 정책을 함께 조정해야함

* Actuator와 Prometheus를 연동해 장기적 캐시 성능 추세를 시각화하면 문제를 조기에 감지할 수 있음

 


정리

항목 설명
Hit Ratio 캐시 효율성을 판단하는 핵심 지표
응답 시간 분석 캐시 적용 전후 성능 비교로 효과 측정
메모리 모니터링 Heap / Garbage Collection 추적 필수
병목 현상 식별 TTL 동시 만료, 캐시 락 주의