본문으로 건너뛰기
김신건의 로그

[Spring] Cache: @Cacheable, @CacheEvict, Redis

· 수정 · 📖 약 1분 · 558자/단어 #spring #cache #redis #caffeine #performance
spring cache, @Cacheable, @CacheEvict, @CachePut, spring cache redis, CaffeineCache

정의

Spring Cache는 메서드 결과를 캐시하는 추상화. @Cacheable 한 줄로 메모이제이션. 백엔드는 Caffeine, Redis, Hazelcast, EhCache 등 자유롭게. 캐시 키 생성, TTL, 무효화를 표준 어노테이션으로.

활성화

implementation("org.springframework.boot:spring-boot-starter-cache")
@SpringBootApplication
@EnableCaching
public class App { }

@Cacheable

@Service
public class UserService {

    @Cacheable("users")
    public User findById(Long id) {
        return userRepository.findById(id).orElseThrow();
    }
}

첫 호출 시 DB 조회 + 캐시 저장. 이후 같은 ID는 캐시에서.

여러 cache 이름:

@Cacheable({"users", "all-users"})

key

@Cacheable(value = "users", key = "#id")
public User findById(Long id) { ... }

@Cacheable(value = "users", key = "#user.id")
public Profile getProfile(User user) { ... }

@Cacheable(value = "users", key = "#a0 + '-' + #a1")
public List<User> search(String name, String role) { ... }

@Cacheable(value = "users", key = "T(java.util.Objects).hash(#name, #role)")
public ...

SpEL 표현식. 기본은 모든 인자 조합으로 SimpleKey 생성.

condition / unless

@Cacheable(value = "users", condition = "#id > 0", unless = "#result == null")
public User findById(Long id) { ... }
  • condition: 호출 전 평가 → false면 캐시 미사용
  • unless: 호출 후 평가 → true면 캐시 안 함

@CacheEvict

@CacheEvict(value = "users", key = "#user.id")
public void update(User user) { ... }

@CacheEvict(value = "users", allEntries = true)
public void clear() { ... }

@CacheEvict(value = "users", beforeInvocation = true)
public void deleteIfExists(Long id) {
    if (userRepository.existsById(id)) userRepository.deleteById(id);
}

beforeInvocation=true는 메서드 실행 전 (실패해도 캐시는 비움).

@CachePut

캐시는 갱신하되 메서드는 항상 실행.

@CachePut(value = "users", key = "#user.id")
public User update(User user) {
    return userRepository.save(user);
}

반환값으로 캐시 갱신.

여러 캐시 동시

@Caching(
    evict = {
        @CacheEvict(value = "users", key = "#user.id"),
        @CacheEvict(value = "users-by-email", key = "#user.email"),
    },
    put = {
        @CachePut(value = "users", key = "#user.id"),
    }
)
public User save(User user) {
    return userRepository.save(user);
}

Cache 백엔드

Caffeine (in-memory, 권장 single instance)

implementation("com.github.ben-manes.caffeine:caffeine")
spring:
  cache:
    type: caffeine
    caffeine:
      spec: maximumSize=10000,expireAfterWrite=10m

LRU + TTL + 통계. 단일 JVM에 최적.

Redis (분산)

implementation("org.springframework.boot:spring-boot-starter-data-redis")
spring:
  cache:
    type: redis
    redis:
      time-to-live: 600000    # 10분
      cache-null-values: false
  data:
    redis:
      host: localhost
      port: 6379

여러 인스턴스 공유. 메모리 큼.

EhCache, Hazelcast 등

거의 안 씀. 보통 Caffeine 또는 Redis.

커스텀 CacheManager

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(10000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats());
        return manager;
    }

    @Bean
    public CacheManager redisCacheManager(RedisConnectionFactory factory) {
        return RedisCacheManager.builder(factory)
            .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))
                .disableCachingNullValues()
                .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())))
            .build();
    }
}

cache 별 다른 TTL:

.withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1)))
.withCacheConfiguration("posts", config.entryTtl(Duration.ofMinutes(5)))

직접 캐시 조작

@Service
public class MyService {
    private final CacheManager cacheManager;

    public void warmup() {
        Cache cache = cacheManager.getCache("users");
        cache.put("key", value);
        Cache.ValueWrapper wrapper = cache.get("key");
        cache.evict("key");
        cache.clear();
    }
}

자주 보는 패턴

Repository 메서드 캐시

@Cacheable(value = "users-by-email", key = "#email")
public Optional<User> findByEmail(String email) {
    return userRepository.findByEmail(email);
}

JPA의 1차 캐시 + 2차 캐시(Hibernate)가 있지만 Spring Cache는 더 명시적·간단.

TTL 짧은 hot path

@Cacheable(value = "popular-posts", key = "'top10'")
public List<Post> getPopular() {
    return postRepository.findTop10ByViewsDesc();
}

캐시 짧게 (1분) + 자주 조회 → DB 부담 크게 감소.

비싼 계산

@Cacheable(value = "recommendations", key = "#userId")
public List<Item> recommendations(Long userId) {
    return mlModel.recommend(userId);    // 1초 이상
}

Conditional cache (사용자별)

@Cacheable(value = "user-feed", key = "#userId", condition = "#userId != null")
public List<Post> feed(Long userId) { ... }

함정

1. self-invocation

@Service
public class MyService {
    public List<Item> findAll() {
        return getById(1);    // 직접 호출! @Cacheable 무시
    }

    @Cacheable("items")
    public Item getById(Long id) { ... }
}

proxy 거치지 않음. 다른 Bean으로 분리.

2. null 캐시

@Cacheable("users")
public User findById(Long id) {
    return userRepository.findById(id).orElse(null);    // null 캐시됨
}

unless = "#result == null"로 방지.

3. Map<K,V> 캐싱

@Cacheable("config")
public Map<String, String> loadConfig() { ... }

거대 Map 통째로 캐싱은 무효화 어려움. 더 작은 단위로.

4. JSON 직렬화

Redis 사용 시 직렬화/역직렬화 비용. final class, generic은 까다로움.

5. Cache stampede

만료 순간 동시 다수 요청 → 모두 DB. lock-based 갱신:

@Cacheable(value = "...", sync = true)
public User findById(Long id) { ... }

sync=true는 첫 요청만 메서드 실행, 나머지 대기.

Cache Statistics

Caffeine:

.recordStats()

// 통계
caffeineCache.stats();
// CacheStats{hitCount=1000, missCount=50, ...}

Micrometer 통합:

management:
  metrics:
    cache:
      instrument: true

cache.gets, cache.puts, cache.evictions 메트릭.

TTL 전략

  • Hot data: TTL 짧음 (분)
  • Reference data: TTL 길게 (시간/일)
  • 무한: TTL 없음 + 명시적 evict
  • LRU: 메모리 한계 시 자동 제거

비교: Redis vs Caffeine

CaffeineRedis
위치JVM 내부외부
분산X (각 인스턴스)O
속도매우 빠름빠름 (네트워크)
메모리JVM heap별도
영속성X옵션
인프라추가 X필요

단일 인스턴스 + read-heavy → Caffeine. 분산 → Redis.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기