Spring Cache Abstraction 완전 학습 인덱스
1. 개요 & 배경
Spring Cache Abstraction이란?
- 캐시 추상화의 목적 — 비즈니스 로직과 캐시 기술 분리
- AOP 기반 선언적 캐싱 (
@Cacheable,@CachePut,@CacheEvict) - 캐시 추상화 vs 직접 캐시 API 사용 비교
- 지원되는 캐시 구현체 목록 (Caffeine, Redis, EhCache, Hazelcast, Infinispan, ConcurrentMap 등)
- Spring Boot Auto Configuration과의 관계
캐싱이 필요한 상황
- DB 쿼리 결과 재사용 (읽기 중심 데이터)
- 외부 API 응답 캐싱
- 무거운 연산 결과 재사용
- 세션/인증 정보 임시 저장
- 캐시 적용 전후 성능 측정 방법
2. 기본 설정
@EnableCaching
@EnableCaching어노테이션 활성화@SpringBootApplication클래스 vs 별도@ConfigurationproxyTargetClass옵션 — CGLib vs JDK 동적 프록시- Spring Boot Auto Configuration (
CacheAutoConfiguration)
CacheManager 선택
ConcurrentMapCacheManager— 기본값, 개발/테스트용CaffeineCacheManager— 로컬 인메모리 (운영 권장)RedisCacheManager— 분산 캐시EhCacheCacheManager— EhCache 2.xJCacheCacheManager— JSR-107(JCache) 통합- 복수 CacheManager 등록 (
CompositeCacheManager)
spring.cache.* 설정
spring.cache.type— 캐시 구현체 명시spring.cache.cache-names— 캐시 이름 사전 등록- 캐시 타입 자동 감지 순서
3. 핵심 어노테이션
@Cacheable
- 기본 사용법 — 메서드 반환값 캐시 저장
value/cacheNames— 캐시 이름 지정key— SpEL로 캐시 키 지정condition— 캐시 저장 조건 (메서드 실행 전 평가)unless— 캐시 저장 제외 조건 (메서드 실행 후 평가)sync— 동시 요청 시 단일 로더 실행 (캐시 스탬피드 방지)cacheManager— 특정 CacheManager 지정keyGenerator— 커스텀 KeyGenerator 지정
@CachePut
- 항상 메서드를 실행하고 결과를 캐시에 갱신
@Cacheablevs@CachePut차이- 생성/수정 시 캐시 동기화 패턴
- 동일
key조건 유지의 중요성
@CacheEvict
- 단일 항목 제거 vs 전체 제거 (
allEntries = true) beforeInvocation— 메서드 실행 전/후 제거 시점 선택- 예외 발생 시 동작 차이 (
beforeInvocationtrue vs false) - 삭제 시나리오에서의 활용 패턴
@Caching
- 여러 캐시 어노테이션 조합
- 복잡한 캐시 갱신 시나리오 (여러 캐시 동시 처리)
- 같은 메서드에
@Cacheable+@CacheEvict조합
@CacheConfig
- 클래스 레벨 공통 캐시 설정
cacheNames,cacheManager,keyGenerator상속- 메서드 레벨 설정이 클래스 레벨을 오버라이드하는 규칙
4. 캐시 키 전략
기본 KeyGenerator
- 매개변수 없음 →
SimpleKey.EMPTY - 매개변수 1개 → 해당 객체 자체
- 매개변수 2개 이상 →
SimpleKey(params...) - 기본 키 충돌 시나리오와 주의사항
SpEL 표현식으로 키 지정
#param— 파라미터명 참조#p0,#a0— 인덱스 기반 참조#root.method,#root.target,#root.args#result— 반환값 참조 (@CachePut,@CacheEvict의unless)- 복합 키:
#userId + ':' + #productId T(...)— 정적 메서드 호출
커스텀 KeyGenerator
KeyGenerator인터페이스 구현@Bean등록 및keyGenerator속성 참조- 메서드 시그니처 기반 자동 키 생성 전략
- 네임스페이스 충돌 방지 패턴
5. Caffeine 캐시 (로컬 인메모리)
Caffeine 소개
- W-TinyLFU 알고리즘 — LRU/LFU의 장점 결합
- Caffeine vs Guava Cache 비교
- 적합한 사용 사례 (단일 서버, 빠른 응답 요구)
기본 설정
spring-boot-starter-cache+com.github.ben-manes.caffeine:caffeinespring.cache.caffeine.spec설정 문자열CaffeineSpec옵션:maximumSize,expireAfterWrite,expireAfterAccess,refreshAfterWrite
캐시별 개별 설정
CaffeineCacheManager직접 빈 등록Caffeine.newBuilder()per-cache 설정CacheLoader— 자동 리로드 (refreshAfterWrite)
통계 수집
recordStats()활성화CacheStats— hitCount, missCount, loadSuccessCount- Micrometer 통합 메트릭 (
cache.gets,cache.size)
6. Redis 캐시 (분산 캐시)
RedisCacheManager 설정
spring-boot-starter-data-redis+spring-boot-starter-cacheRedisCacheConfiguration— 기본 TTL, 직렬화, 키 prefixRedisCacheManager.builder()구성
직렬화 전략
- 기본
JdkSerializationRedisSerializer— 호환성 문제 GenericJackson2JsonRedisSerializer— 타입 정보 포함 JSONJackson2JsonRedisSerializer— 타입 고정 JSON (더 빠름)- 직렬화 버전 불일치 문제와 해결 방법
캐시별 TTL 설정
RedisCacheManagerBuilderCustomizerwithInitialCacheConfigurations()— 캐시별 개별 설정- 동적 TTL 설정 패턴
키 네임스페이스
computePrefixWith()— 캐시 이름 prefix 전략- 애플리케이션 버전 포함 키 설계
- 멀티 테넌트 환경에서의 키 분리
Redis 캐시 운영
KEYS,SCAN명령어로 캐시 확인- TTL 만료 이벤트 (
notify-keyspace-events) - Redis Cluster 환경에서의 캐시 주의사항
7. 캐시 동기화 & 일관성
캐시 스탬피드 (Cache Stampede) 문제
- 동시에 캐시 미스 발생 시 DB 과부하
@Cacheable(sync = true)— Spring 4.3+- 분산 환경에서의 한계와 Redis 분산 락 패턴
- Caffeine의
refreshAfterWrite활용
캐시 무효화 전략
- TTL 기반 자동 만료
- 이벤트 기반 명시적 제거 (
@CacheEvict) @TransactionalEventListener와 조합 — 트랜잭션 커밋 후 제거- Write-Through vs Write-Behind vs Cache-Aside 패턴
분산 환경 일관성
- 다중 인스턴스에서 로컬 캐시 무효화 문제
- Redis Pub/Sub을 이용한 로컬 캐시 동기화
- 2-Tier 캐시 (Caffeine + Redis) 아키텍처
- 이벤트 기반 캐시 동기화 구현
8. 프로그래밍 방식 캐시 제어
CacheManager & Cache API
CacheManager.getCache(name)— 캐시 인스턴스 직접 획득Cache.get(key)/Cache.put(key, value)/Cache.evict(key)Cache.get(key, Callable)— 캐시 미스 시 로더 실행Cache.putIfAbsent(key, value)
사용 사례
- 어노테이션 사용이 어려운 동적 키 처리
- 배치 처리에서의 캐시 제어
- 캐시 워밍(Cache Warming) 구현
- 애플리케이션 시작 시 캐시 사전 로드 (
ApplicationRunner)
9. 자기 호출(Self-Invocation) 문제
문제 원인
- Spring AOP 프록시 기반 캐시 동작 원리
- 같은 빈 내부 메서드 호출 시 프록시 우회
@Cacheable메서드가 동일 클래스의 다른 메서드에서 호출되는 경우
해결 방법
- 별도 빈으로 분리 (가장 권장)
ApplicationContext에서 자기 자신 빈 획득 (안티패턴)AopContext.currentProxy()활용- AspectJ 위빙으로 전환 (컴파일/로드 타임)
10. 테스트
캐시 테스트 전략
@SpringBootTestvs@DataCacheTest(없음 — 직접 구성 필요)- 테스트에서
ConcurrentMapCacheManager사용 - 캐시 히트/미스 검증 방법 (호출 횟수 검증)
MockitoExtension + 캐시
CacheManager모킹@MockBean CacheManager로 캐시 비활성화@Cacheable어노테이션 동작 자체 테스트
캐시 초기화
@DirtiesContext— 테스트 간 컨텍스트 재생성 (느림)- 각 테스트에서
CacheManager.getCache(name).clear() @BeforeEach에서 캐시 수동 초기화
Testcontainers + Redis
@ServiceConnection으로 Redis 컨테이너 자동 연결- 실제 Redis 캐시 통합 테스트
- TTL 동작 검증
11. 모니터링 & 운영
Micrometer 메트릭 자동 수집
cache.gets(tags: name, cacheManager, result=hit/miss/pending)cache.putscache.evictionscache.size- Prometheus 쿼리 예시: 히트율 계산
Actuator /actuator/caches
- 등록된 캐시 목록 조회
DELETE /actuator/caches/{name}— 특정 캐시 전체 제거- 캐시 상태 모니터링 대시보드
캐시 히트율 목표 설정
- 적정 TTL 결정 기준
- 캐시 크기(maximumSize) 튜닝
- 히트율 저하 알람 설정
12. 고급 패턴
2-Tier 캐시 (로컬 + 분산)
- Caffeine(L1) + Redis(L2) 조합 아키텍처
CompositeCacheManager구성- 캐시 계층 간 데이터 흐름
- L1 만료 후 L2 조회 패턴 구현
조건부 캐시 무효화
- 비즈니스 로직 기반
condition/unless조합 - 사용자 역할에 따른 캐시 분리
- 멀티 테넌트 캐시 키 설계
캐시 웜업 (Cache Warming)
ApplicationRunner/CommandLineRunner로 시작 시 사전 로드- 배포 후 콜드 스타트 문제 완화
- 점진적 캐시 워밍 전략
커스텀 CacheManager 구현
AbstractCacheManager상속- 동적 캐시 생성 (
allowNullValues,createMissingCache) - 캐시 이름 기반 설정 자동 적용 패턴
13. JSR-107 (JCache) 통합
JCache 어노테이션
@javax.cache.annotation.CacheResult(=@Cacheable)@CachePut/@CacheRemove/@CacheRemoveAll- Spring Cache 어노테이션과의 차이점
- 상호 운용성 — 같은 캐시를 양쪽에서 접근
JCache 구현체 연동
- EhCache 3.x (JCache 지원)
- Hazelcast
- Infinispan
JCacheCacheManager설정
14. 실전 예제 & 베스트 프랙티스
자주 쓰이는 패턴
- 사용자 프로필 캐싱 (TTL: 5분)
- 상품 목록/상세 캐싱 (TTL: 1시간)
- 권한/역할 캐싱 (TTL: 10분)
- API 응답 캐싱 (외부 서비스 장애 대비)
- 설정값 캐싱 (DB 조회 최소화)
캐시 설계 원칙
- 캐시 키는 의미 있고 고유해야 한다
null값 캐싱 여부 결정 (allowNullValues)- 직렬화 가능한 Value 객체 설계 (Redis 캐시 시 필수)
- 캐시 의존성이 없는 비즈니스 로직 작성
주의할 안티패턴
- 트랜잭션 내부에서
@CacheEvict(커밋 전 제거 위험) - 변경이 잦은 데이터 캐싱 (일관성 문제)
- 캐시 크기 무한 허용 (OOM 위험)
- 너무 긴 TTL (stale 데이터 노출)
@Cacheable과@Transactional순서 혼동