CacheManager — 캐시 구현체를 갈아끼우는 방법
로컬 캐시에서 Redis 캐시로 전환할 때, 비즈니스 코드를 하나도 안 바꿀 수 있을까요?
Spring의 캐시 추상화가 빛을 발하는 순간이 바로 이때입니다. CacheManager만 교체하면 @Cacheable 코드는 그대로 유지하면서 캐시 구현체를 자유롭게 바꿀 수 있습니다.
CacheManager란
CacheManager는 Cache 인스턴스를 생성하고 관리하는 팩토리 인터페이스입니다.
public interface CacheManager {
Cache getCache(String name); // 이름으로 Cache 조회
Collection<String> getCacheNames(); // 관리 중인 캐시 이름 목록
}
@Cacheable("products")가 실행되면 내부적으로 cacheManager.getCache("products")를 호출하여 Cache 인스턴스를 가져오고, 그 Cache에 데이터를 조회/저장합니다.
ConcurrentMapCacheManager — 가장 단순한 로컬 캐시
JDK의 ConcurrentHashMap을 기반으로 하는 인메모리 캐시입니다.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
ConcurrentMapCacheManager manager = new ConcurrentMapCacheManager();
manager.setCacheNames(List.of("products", "users"));
return manager;
}
}
특징:
- 별도 의존성 없이 바로 사용 가능
- TTL(만료 시간) 설정 불가
- 최대 크기 제한 없음 (메모리 누수 위험)
- 단일 JVM에서만 유효
** 용도:** 개발/테스트 환경, 프로토타이핑
Caffeine — 고성능 로컬 캐시
Caffeine은 Google Guava Cache의 후속 프로젝트로, 높은 적중률과 성능을 자랑하는 로컬 캐시 라이브러리입니다.
의존성 추가
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
기본 설정
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
캐시별 개별 설정
실무에서는 캐시마다 다른 정책이 필요합니다. 자주 변하는 데이터는 짧은 TTL, 거의 변하지 않는 데이터는 긴 TTL을 설정합니다.
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager manager = new SimpleCacheManager();
manager.setCaches(List.of(
buildCache("products", 500, 10, TimeUnit.MINUTES),
buildCache("users", 1000, 30, TimeUnit.MINUTES),
buildCache("config", 100, 1, TimeUnit.HOURS)
));
return manager;
}
buildCache() 헬퍼 메서드에서 Caffeine 빌더로 캐시별 최대 크기, TTL, 통계 수집 여부를 설정합니다.
private CaffeineCache buildCache(String name, int maxSize,
long duration, TimeUnit unit) {
return new CaffeineCache(name,
Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(duration, unit)
.recordStats() // 캐시 통계 수집
.build()
);
}
}
Caffeine의 주요 설정:
| 설정 | 설명 |
|---|---|
maximumSize | 최대 엔트리 수 (초과 시 LRU 방식으로 퇴거) |
expireAfterWrite | 쓰기 후 만료 시간 |
expireAfterAccess | 마지막 접근 후 만료 시간 |
refreshAfterWrite | 쓰기 후 갱신 트리거 (비동기 리프레시) |
recordStats | 히트율, 미스율 등 통계 수집 |
RedisCacheManager — 분산 캐시
여러 서버가 동일한 캐시를 공유해야 하는 분산 환경에서는 Redis를 사용합니다.
의존성과 기본 설정
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
커스텀 RedisCacheManager 설정
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 기본 캐시 설정
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues(); // null 값 캐싱 방지
기본 설정을 기반으로 캐시별 TTL을 개별 지정하고, transactionAware()로 트랜잭션과 동기화합니다.
// 캐시별 개별 설정
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"products", defaultConfig.entryTtl(Duration.ofMinutes(30)),
"users", defaultConfig.entryTtl(Duration.ofHours(1)),
"sessions", defaultConfig.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware() // 트랜잭션과 동기화
.build();
}
}
직렬화 설정의 중요성
Redis는 외부 저장소이므로 객체를 직렬화해서 저장해야 합니다. 직렬화 방식에 따라 성능, 호환성, 디버깅 편의성이 달라집니다.
| 직렬화 방식 | 장점 | 단점 |
|---|---|---|
JdkSerializationRedisSerializer | 기본값, 별도 설정 불필요 | 가독성 낮음, 클래스 변경 시 역직렬화 실패 |
GenericJackson2JsonRedisSerializer | JSON 형태로 가독성 좋음 | 타입 정보 포함으로 용량 약간 증가 |
Jackson2JsonRedisSerializer | 지정 타입만 직렬화 | 캐시마다 개별 설정 필요 |
StringRedisSerializer | 키 직렬화에 적합 | 값 직렬화에는 부적합 |
실무에서는 키는 StringRedisSerializer, 값은 GenericJackson2JsonRedisSerializer를 사용하는 것이 일반적입니다.
다중 CacheManager
로컬 캐시와 분산 캐시를 동시에 사용하는 경우가 있습니다. 변경이 거의 없는 설정 데이터는 로컬 캐시로, 여러 서버가 공유해야 하는 데이터는 Redis로 관리하는 패턴입니다.
@Configuration
@EnableCaching
public class MultiCacheConfig {
@Bean
@Primary
public CacheManager caffeineCacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(5, TimeUnit.MINUTES));
return manager;
}
@Primary가 붙은 Caffeine이 기본 CacheManager가 되고, Redis는 명시적으로 지정할 때만 사용합니다.
@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)))
.build();
}
}
@Service
public class ProductService {
// 기본(@Primary) CacheManager 사용 → Caffeine
@Cacheable("localConfig")
public AppConfig getConfig() { ... }
// 명시적으로 Redis CacheManager 지정
@Cacheable(value = "products", cacheManager = "redisCacheManager")
public Product findById(Long id) { ... }
}
캐시 통계 모니터링
Caffeine의 recordStats()와 Spring Boot Actuator를 연동하면 캐시 통계를 모니터링할 수 있습니다.
management:
endpoints:
web:
exposure:
include: caches, metrics
/actuator/metrics/cache.gets로 히트율/미스율을 확인할 수 있습니다. 캐시 히트율이 낮다면 TTL이나 캐시 키 전략을 재검토해야 합니다.
구현체 선택 가이드
| 상황 | 추천 구현체 |
|---|---|
| 개발/테스트 | ConcurrentMapCacheManager |
| 단일 서버, 고성능 필요 | Caffeine |
| 분산 환경, 서버 간 공유 | Redis |
| 로컬 + 분산 혼합 | Caffeine + Redis (다중 CacheManager) |
주의할 점
1. 로컬 캐시에서 Redis 캐시로 전환할 때 직렬화 문제가 발생한다
Caffeine 같은 로컬 캐시는 객체 참조를 그대로 저장하지만, Redis는 직렬화가 필수입니다. 기본 JdkSerializationRedisSerializer는 클래스 구조가 변경되면 역직렬화에 실패합니다. 캐시 구현체를 교체할 때는 저장 대상 객체의 직렬화 호환성을 반드시 점검해야 합니다.
2. 다중 CacheManager 환경에서 @Primary를 지정하지 않으면 빈 충돌이 발생한다
CacheManager 빈이 2개 이상이면 Spring이 어떤 것을 기본으로 사용할지 알 수 없어 NoUniqueBeanDefinitionException이 발생합니다. 반드시 하나에 @Primary를 붙이고, 나머지는 @Cacheable(cacheManager = "빈이름")으로 명시적으로 지정해야 합니다.
3. ConcurrentMapCacheManager를 운영 환경에서 사용하면 메모리 누수가 발생한다
ConcurrentMapCacheManager는 TTL과 최대 크기 제한이 없습니다. 캐시 데이터가 계속 쌓여 OOM(OutOfMemoryError)이 발생할 수 있습니다. 개발/테스트에서만 사용하고, 운영 환경에서는 반드시 Caffeine이나 Redis로 교체해야 합니다.
정리
- CacheManager 는 Cache 인스턴스를 관리하는 팩토리로, 교체만으로 캐시 구현체를 변경할 수 있습니다
- Caffeine 은 로컬 환경에서 TTL, 최대 크기, 통계 수집을 지원하는 고성능 캐시입니다
- Redis 는 분산 환경에서 캐시를 공유할 수 있으며, 직렬화 설정이 핵심입니다
- 다중 CacheManager로 로컬/분산 캐시를 함께 사용할 수 있고,
@Cacheable의cacheManager속성으로 지정합니다