스프링

Spring Cache 깊게 파헤치기: Cache vs CacheManager

Stitchhhh 2026. 2. 26. 10:30

Spring에서 캐시를 사용할 때 @Cacheable 어노테이션 한 줄로 마법처럼 동작하는 배경에는 CacheManagerCache라는 두 핵심 인터페이스의 철저한 역할 분담이 있습니다. 이 둘의 관계를 이해하면 커스텀 캐싱 전략을 훨씬 유연하게 설계할 수 있습니다.


1. CacheManager: 캐시를 찍어내는 "공장이자 관리소"

CacheManager는 캐시 추상화의 최상위 진입점입니다. 애플리케이션 내의 모든 캐시 인스턴스를 생성하고 관리하는 역할을 합니다.

  • 주요 역할:
    • 캐시 생성 및 조회: "users", "products" 등 이름에 맞는 Cache 객체를 생성하거나 찾아줍니다.
    • 설정의 중심: 데이터 유효 기간(TTL), 최대 크기, 직렬화 방식 등 '어떻게(How)' 저장할지를 결정합니다.
    • 라이프사이클 관리: 앱 시작 시 초기화하고 종료 시 리소스를 정리합니다.
  • 대표적인 구현체:
    • CaffeineCacheManager: 고성능 로컬 캐시 (Caffeine 기반)
    • RedisCacheManager: 분산 환경을 위한 Redis 기반 캐시
    • CompositeCacheManager: 여러 매니저를 묶어 우선순위대로 처리 (Local + Remote 하이브리드)

2. Cache: 데이터가 저장되는 "실제 저장소"

Cache는 특정 이름(예: "products")을 가진 개별 저장소의 추상화된 뷰(View)입니다. 내부적으로 Map과 유사하게 Key-Value 쌍을 다룹니다.

  • 주요 역할:
    • CRUD 작업: 데이터를 읽고(get), 쓰고(put), 삭제(evict)하는 실질적인 연산을 수행합니다.
    • 스토어 어댑터: 개발자가 Redis나 Caffeine의 API를 직접 몰라도, Spring의 공통 인터페이스로 조작할 수 있게 해줍니다.
    • 데이터 격리: 각 캐시 인스턴스는 서로 독립적이라 데이터가 섞이지 않습니다.

3. 상호작용 흐름 (How they work together)

@Cacheable(value = "users", key = "#id")가 호출될 때의 내부 흐름은 다음과 같습니다.

  1. 요청: AOP 인터셉터가 가로채서 CacheManager에게 "이름이 users인 캐시를 달라"고 요청합니다.
  2. 반환: CacheManager는 설정된 방식에 맞는 Cache 객체(예: RedisCache)를 반환합니다.
  3. 조회/저장: 반환받은 Cache 객체에게 "키가 #id인 데이터가 있는지 확인하고, 없으면 DB 결과를 저장해"라고 명령합니다.

4.  Kotlin 코드로 보는 실전 비교

어떤 캐시 전략을 사용할지 정의하는 단계입니다.

@Configuration
@EnableCaching
class CacheConfig {

    @Bean
    fun cacheManager(redisConnectionFactory: RedisConnectionFactory): CacheManager {
        // RedisCacheManager는 '어떻게' 저장할지(예: TTL 1시간)를 결정하여
        // Cache 인스턴스를 찍어내는 공장 역할을 합니다.
        val configuration = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(1)) // TTL 설정
            .disableCachingNullValues()    // null 저장 방지

        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(configuration)
            .build()
    }
}

 

필요에 따라 수동으로 데이터를 조작할 때의 방식입니다.

@Service
class ManualCacheService(
    private val cacheManager: CacheManager // 1. 매니저 주입
) {
    fun forceClearCache() {
        // 2. 매니저에게서 특정 'Cache' 인스턴스를 획득
        val productCache: Cache? = cacheManager.getCache("products")

        // 3. 획득한 Cache 객체로 실제 데이터 조작 (전체 삭제)
        productCache?.clear()
    }

    fun manuallyPut(key: String, value: Any) {
        val cache = cacheManager.getCache("products")
        // 직접 데이터 삽입
        cache?.put(key, value)
    }
}