자바 백엔드 입문 시리즈 59편 중 56편. @Cacheable·@CacheEvict로 메서드 결과를 자동 캐싱하는 표준 패턴과 Redis 연동, 키 전략·TTL·자기 호출 함정까지 풀어쓴 학습 노트.
이 글은 자바 백엔드 입문 시리즈 59편 중 56편이에요. 55편 @Scheduled 가 "반복 실행" 이었다면, 이번 56편은 "같은 결과를 매번 DB 안 가고 메모리에서 반환" 의 표준 패턴 — @Cacheable 로 메서드 결과를 자동 캐싱.
@Cacheable — 메서드 결과 자동 캐싱
같은 매개변수로 호출된 메서드 — 두 번째부터는 "실제 메서드 실행 안 하고 메모리에서 반환" 하는 패턴.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository productRepo;
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
// 첫 호출: DB 조회 → 결과 캐시 저장
// 두 번째부터: 캐시에서 즉시 반환 (DB 안 감)
return productRepo.findById(id).orElseThrow();
}
}
활성화는 @EnableCaching 한 줄.
@SpringBootApplication
@EnableScheduling
@EnableCaching
public class MyApp { ... }
내부 동작 = 23편 AOP 프록시. Spring이 메서드 호출을 가로채 "캐시 있으면 반환, 없으면 메서드 실행 후 저장".
3가지 캐시 어노테이션
| 어노테이션 | 동작 |
|---|---|
@Cacheable |
캐시 있으면 반환, 없으면 메서드 실행 후 저장 |
@CachePut |
항상 메서드 실행 + 결과를 캐시에 갱신 |
@CacheEvict |
캐시 무효화 (삭제) |
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) { ... }
@CachePut(value = "products", key = "#product.id")
public Product update(Product product) {
return productRepo.save(product); // 항상 실행 + 캐시 갱신
}
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepo.deleteById(id); // 캐시도 함께 삭제
}
@CacheEvict(value = "products", allEntries = true)
public void deleteAll() { ... } // 전체 캐시 무효화
세 어노테이션의 조합이 일반 패턴 — 조회는 @Cacheable, 수정은 @CachePut, 삭제는 @CacheEvict.
key — SpEL로 동적 키
캐시 키 생성에 22편 SpEL 활용.
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) { ... }
@Cacheable(value = "search", key = "#status + '_' + #page")
public Page<Product> search(String status, int page) { ... }
@Cacheable(value = "users", key = "#root.methodName + '_' + #user.id")
public UserDetail getDetail(User user) { ... }
#매개변수이름 = 매개변수 참조. #root.methodName = 메서드 이름 자체.
condition·unless — 조건부 캐싱
@Cacheable(value = "products", key = "#id",
condition = "#id > 0", // 양수일 때만 캐시
unless = "#result == null") // null 결과는 캐시 X
public Product findById(Long id) { ... }
캐시 저장소 — Local·Redis·Memcached
기본 캐시 저장소는 JVM 메모리(Caffeine 등). 한 인스턴스에 한정.
분산 환경 = Redis 연동.
spring:
cache:
type: redis
data:
redis:
host: localhost
port: 6379
spring-boot-starter-data-redis 의존성 추가 + application.yml 설정. 그러면 @Cacheable 캐시 저장소가 자동으로 Redis로 변경. 모든 서버 인스턴스가 같은 Redis 공유.
TTL — 캐시 만료 시간
Redis 캐시는 TTL(Time To Live) 설정 가능.
spring:
cache:
redis:
time-to-live: 600000 # 10분
10분이 지나면 자동 만료 → 다음 호출 시 메서드 재실행 후 캐시 갱신.
캐시별로 다른 TTL 설정:
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.withCacheConfiguration("products",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)))
.withCacheConfiguration("hot",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5)))
.build();
}
캐시 함정 — 자기 호출
23편 AOP 의 자기 호출 함정이 @Cacheable 에도 똑같이.
@Service
public class ProductService {
public Product findByCode(String code) {
Long id = lookupId(code);
return findById(id); // ❌ 같은 클래스 메서드 호출 — 캐시 안 먹힘
}
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) { ... }
}
해결 = 다른 클래스로 분리. 일반적인 AOP 함정과 동일.
변경 거의 없는 데이터(상품 카탈로그·설정값)는 캐시 적합. 실시간성 중요한 데이터(주문 상태·잔고)는 캐시 위험 — 옛 데이터 노출. "읽기 빈도가 쓰기 빈도보다 훨씬 많고, 약간의 지연된 데이터를 허용 가능" 한 시나리오에만.
한 줄 정리 — @EnableCaching + @Cacheable·@CachePut·@CacheEvict 세 어노테이션으로 메서드 결과 자동 캐싱. Redis 연동으로 분산 캐시. TTL·키 전략·자기 호출 함정 주의.
시험 직전 한 번 더 — @Cacheable 입문자가 매번 헷갈리는 것
@EnableCaching= 메인 클래스에 박아 활성화@Cacheable= 캐시 있으면 반환, 없으면 메서드 실행 + 저장@CachePut= 항상 실행 + 결과를 캐시에 갱신@CacheEvict= 캐시 무효화value = "캐시이름"= 캐시 그룹key = "#매개변수"= SpEL로 동적 키condition/unless= 조건부 캐싱- 기본 캐시 = JVM 메모리 (Caffeine 등)
- 분산 환경 = Redis 연동
application.yml의spring.cache.type=redis- TTL =
spring.cache.redis.time-to-live - 캐시별 다른 TTL =
CacheManagerBean 직접 정의 - 자기 호출 함정 = 같은 클래스 메서드 호출 시 캐시 안 먹힘
- 해결 = 다른 클래스로 분리
- 캐시 적합 = 읽기 빈도 ≫ 쓰기 빈도 + 약간의 지연 데이터 OK
- 캐시 비적합 = 주문 상태·잔고 같은 실시간 데이터
- AOP 프록시 기반 동작 — public 메서드만 지원
- 캐시 키 생성 실수 = 모든 호출이 같은 키 → 잘못된 결과 반환
- 캐시 무효화 정책 = 적극적인
@CacheEvict또는 짧은 TTL - 캐시는 "마지막 최적화 도구" — 먼저 쿼리·인덱스·N+1 개선
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 51편 — 영속성 컨텍스트와 LazyLoading
- 52편 — @SpringBootTest 통합 테스트
- 53편 — MockMvc로 컨트롤러 테스트
- 54편 — Testcontainers 실제 DB 통합 테스트
- 55편 — @Scheduled로 작업 스케줄링
다음 글: