최적화하기
- 로컬 캐시(Local Cache)의 크기 제한과 메모리 사용량 고려 기준 이해하기
- 시간 기반(Time-Based) 및 접근 기반(Access-based) 만료 정책 설정법
- 캐시 항목의 수명을 효율적으로 관리하는 전략
- 캐시 항목 제거(Expire, Evict) 이벤트를 감지하고 후처리를 구현하는 법
- 실무 환경에서 캐시 최적화를 적용하는 법
로컬 캐시 구성 최적화
로컬 캐시는 애플리케이션 내부에서 직접 데이터를 저장하기 때문에 빠르지만,
무한정 데이터를 쌓을 경우 메모리 과다 사용으로 이어질 수 있음
따라서 캐시를 효과적으로 사용하기 위해서는 크기 제한, 만료 정책, 제거 이벤트 처리를 적절히 조합해야함
1. 최적화의 필요성
캐시를 잘못 구성하면 여러 문제가 발생할 수 있다
- OutOfMemoryError(메모리 부족 에러)
- 오래된 데이터 유지로 인한 불일치 문제
- 캐시 적중률 하락으로 인한 불필요한 DB 부하
캐시 크기 제한
1. 크기 제한의 필요성
캐시의 크기를 제한하지 않으면, 요청이 많을수록 캐시 항목이 계속 쌓일 수 있다.
이는 JVM 힙 메모리르 점점 압박하여 GC부하 및 시스템 지연을 초래할 수 있음 - Spring은 순수 자바로 변환 가능
따라서 maximumSize() 또는 maximumWeight() 옵션을 설정해서 캐시 항목의 개수나 메모리 사용량을 제한해야함
2. Caffeine을 이용한 크기 제한 설정 예시
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 별도의 Bean 정의가 없으면, ConcurrentMapCacheManager가 기본적으로 사용됨
public class CacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("userCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES));
return cacheManager;
}
}
* maximumSize(1000) : 캐시에 저장되는 최대 항목수를 1000개로 제한함
- maximumSize(long size)
* 초과된 항목은 자동으로 제거됨(Eviction)
| maximumSize(long size) | 캐시에 저장될 최대 항목 수 지정 |
| maximumWeight(long weight) | 각 항목의 가중치 기반 제한(custom weight 설정 필요) |
팁
* 캐시 크기는 요청 빈도 x 데이터 크기 x 평균 수명을 고려해서 설정함
* 너무 작은 크기는 적중률(Cache Hit Ratio)을 떨어트리고, 너무 큰 크기는 GC 부하를 유발함
* 애플리케이션 프로파일별로 캐시 크기를 다르게 설정하는것도 좋은 전략이라고 함
만료 정책 설정(Expiration Policy)
캐시는 일정 시간이 지나면 데이터를 자동으로 제거해야함
이렇게 함으로 오래된 데이터를 자동으로 정리할 수 있음
1. 시간 기반 만료(Time-Based Expiration)
시간을 기준으로 캐시 항목을 자동으로 제거하는 정책
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)); // 여기, 쓰기 후 10분이 지나면 만료
설정 메서드
expireAfterWrite(time, unit) : 캐시 항목이 생성(쓰기)된 후 일정 시간이 지나면 만료됨
2. 접근 기반 만료(Access-Based Expiration)
최근 접근이 많았던 데이터를 우선 보관하고, 오래 접근하지 않은 데이터를 제거하는 정책
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(10, TimeUnit.MINUTES)); // 여기
expireAfterAccess 설정은 LRU(Least Recently Used) 방식으로 작동한다고 함
LRU와 LFU의 차이점
| 알고리즘 | 기준 | 특징 |
| LRU(Least Recently Used) | 최근 접근 시점 | 최근 사용한 항목은 유지, 오래된 항목은 제거함 |
| LFU(Least Frequency Used) | 접근 횟수 | 자주 사용된 항목을 유지, 거의 접근하지 않은 항목은 제거함 |
팁
* 정적 데이터(국가 코드, 지역 목록 등)는 expireAfterWrite를 사용한다고 함
* 사용자 세션/토큰 등은 expireAfterAccess로 관리하면 효율적이다
* 캐시 만료 정책은 서비스의 특성에 따라 혼합하여 설정할 수 있음
캐시 항목 제거 리스너
1. 제거 이벤트 감지
캐시 항목이 제거될 때 후처리를 수행해야하는 경우가 있음
DB 동기화, 로그 기록, 또는 이벤트 알림과 같은 작업이 필요할 수 있다
예제 - Caffeine의 removalListener()를 활용해서 캐시 제거 이벤트를 처리하는 예제
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import com.github.benmanes.caffeine.cache.RemovalListener;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching // 별도의 Bean 정의가 없으면, ConcurrentMapCacheManager가 기본적으로 사용됨
public class CacheConfig {
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("userCache");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // 최대 캐시 항목 수
.expireAfterWrite(5, TimeUnit.MINUTES) // write하고 5분 후 만료
.removalListener(new CustomCacheRemovalListener())); // 리스너 메서드를 등록함
return cacheManager;
}
}
// 캐시 항목 제거 리스너 클래스, implements는 caffeine.cache
class CustomCacheRemovalListener implements RemovalListener<Object, Object> {
@Override
public void onRemoval(@Nullable Object key, @Nullable Object value, RemovalCause cause) {
System.out.println("[Cache Removal] key : " + key);
System.out.println("value : " + value);
System.out.println("cause : " + cause);
// 후처리 로직 예시
switch(cause) {
case EXPIRED -> System.out.println("캐시가 만료되어 제거되었습니다.");
case SIZE -> System.out.println("캐시 크기 제한으로 제거되었습니다.");
case EXPLICIT -> System.out.println("명시적으로 제거되었습니다. @CacheEvict 등..");
default -> System.out.println("기타 이유로 제거되었습니다.");
}
// 필요시 추가 로깅 또는 DB 동기화 로직 수행 가능
}
}
설명
| 요소 | 설명 |
| CacheManager | Spring 캐시 매니저, 현재 Caffeine 캐시 관리중 |
| Caffeine.newBuilder() | 캐시의 최대 크기, 만료 정책, 리스너 등 설정 |
| removalListener() | 캐시항목이 제거되면 호출되는 리스너 등록 |
| RemovalCause | 항목이 제거된 이유(EXPIRED, SIZE, EXPLICIT 등) 제공 |
실행하면, 캐시 항목이 만료되거나 크기 제한을 초과하면 콘솔에 로그가 출력된다고 한다.. 테스트 해봤는데 아직 잘 출력이 안된다.
다음 시간에 해보자!
2. 제거 원인(RemovalCause)
| 원인 | 설명 |
| EXPLICIT | 명시적으로 제거됨(@CacheEvict 호출 등) |
| REPLACED | 새로운 값으로 대체됨 |
| COLLECTED | GC에 의해 수거됨 |
| EXPIRED | TTL이 만료되어 제거되었음 |
| SIZE | 크기 제한 초과로 제거됨 |
팁
* 캐시 제거 이벤트는 비즈니스 로직의 핵심에 직접 연결하지 말고, 비동기 로깅 또는 모니터링 용도로만 활용하기
* Caffeine의 removalListener()는 동기적으로 실행되므로, 장시간 블로킹 작업은 별도 쓰레드에서 처리한다!
정리
| 항목 | 설명 |
| 캐시 크기 제한 | 메모리 사용량을 조절하고 OutOfMemory 방지 |
| 시간 기반 만료 | 일정 시간이 지나면 자동으로 만료됨 |
| 접근 기반 만료 | 최근 접근 빈도를 기준으로 오래된 데이터 제거 |
| 제거 리스너 | 캐시 항목 제거 시 후처리 수행(동기) |
'Spring Boot > Cache' 카테고리의 다른 글
| 캐시 모니터링과 문제 해결 : Spring Boot Actuator를 활용한 모니터링 (0) | 2026.01.15 |
|---|---|
| 로컬 캐시 구현과 최적화 : 로컬 캐시 성능 측정 (0) | 2026.01.12 |
| 로컬 캐시 구현과 최적화 : 로컬 캐시 구현 옵션 (0) | 2026.01.12 |
| Spring Cache 기본 사용 : SpEL을 활용한 동적 캐시 키 (0) | 2026.01.10 |
| Spring Cache 기본 사용 : Spring Cache 활성화 및 주요 캐시 애너테이션 (0) | 2026.01.10 |