본문 바로가기

Spring Boot/Cache

로컬 캐시 구현과 최적화 : 로컬 캐시 구성 최적화

최적화하기

 

 - 로컬 캐시(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(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 방지
시간 기반 만료 일정 시간이 지나면 자동으로 만료됨
접근 기반 만료 최근 접근 빈도를 기준으로 오래된 데이터 제거
제거 리스너 캐시 항목 제거 시 후처리 수행(동기)