@Cacheable을 붙였는데 캐시가 전혀 안 되는 것처럼 보인 이유
문제 발생
조회 결과를 캐싱하려고 @Cacheable을 붙였는데, 로그를 보니 캐시에 있어야 할 값도 매번 실제 DB 쿼리가 다시 실행되고 있었습니다.
@Service
public class ProductService {
@Cacheable("products")
public Product findById(Long id) {
return repository.findById(id).orElseThrow();
}
public void doSomething(Long id) {
Product p = findById(id); // 같은 클래스 안에서 직접 호출 — 캐싱 안 됨
}
}원인 분석
Spring의 @Cacheable(그리고 @Transactional, @Async도 마찬가지)은 AOP 프록시를 통해 동작합니다. Spring이 ProductService를 빈으로 등록할 때, 실제로는 원본 클래스를 감싼 프록시 객체를 만들고 그 프록시가 캐싱 로직(조회 전 캐시 확인, 결과를 캐시에 저장)을 수행한 뒤 실제 메서드를 호출합니다.
문제는 이 프록시가 외부에서 그 빈을 호출할 때만 개입한다는 점입니다. doSomething() 안에서 findById(id)를 호출하는 것은 this.findById(id)와 같은 뜻이고, this는 프록시가 아니라 원본 객체 자신을 가리킵니다 — 프록시를 거치지 않고 메서드가 직접 호출되므로, 캐싱 로직이 통째로 우회됩니다. 이걸 self-invocation(자기 호출) 문제라고 부르며, Spring 공식 문서가 AOP 프록시 기반 기능들의 공통 제약으로 명시하고 있습니다.
해결 방안
- 가장 근본적인 해결책은 설계를 바꾸는 것입니다 — 캐싱이 필요한 메서드를 별도의 빈으로 분리해 외부에서 호출하도록 만듭니다.
@Service
public class ProductService {
private final ProductCacheService cacheService;
public void doSomething(Long id) {
Product p = cacheService.findById(id); // 다른 빈을 거치므로 프록시가 개입함
}
}
@Service
public class ProductCacheService {
@Cacheable("products")
public Product findById(Long id) { ... }
}- 구조를 바꾸기 어렵다면, 스프링 컨텍스트에서 자기 자신의 프록시를 주입받아 그걸 통해 호출하는 방법도 있습니다(
@Lazy자기 참조 주입, 또는AopContext.currentProxy()— 후자는exposeProxy=true설정이 추가로 필요합니다). 다만 이 방식은 코드가 다소 어색해지므로, 가능하면 1번처럼 메서드를 다른 빈으로 옮기는 것이 더 권장됩니다. @Transactional,@Async도 완전히 동일한 제약을 받습니다 — 하나를 이해하면 셋 다 같은 원리로 디버깅할 수 있습니다.- 이 문제는 컴파일이나 실행 자체는 정상적으로 되고 그냥 "기능이 조용히 동작 안 하는" 형태로 나타나므로, 캐싱/트랜잭션/비동기 관련 애너테이션이 안 먹히는 것 같다면 항상 self-invocation부터 의심하는 것이 진단 순서상 효율적입니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.