본문 바로가기

WEB개발/Spring

AOP @Cacheable @CacheEvict

문제상황
 
하나의 트랜잭션에서 캐시를 삭제하고 다시 재조회를 하려는데 캐시가 지워지지 않았다
 
=> 이건 실무에서도 자주 겪는 “한 트랜잭션 내 캐시 반영 지연 문제”입니다.
핵심은 👉 Spring의 캐시 동작은 트랜잭션 커밋 시점과는 무관하다는 점이에요.
@Service
public class UserService {

    @CacheEvict(value = "user", key = "#id")
    public void evict(Long id) {
        // 캐시 삭제
    }

    @Cacheable(value = "user", key = "#id")
    public User find(Long id) {
        return repository.findById(id).orElseThrow();
    }

    public User process(Long id) {
        evict(id);     // ❌ 자기 자신 내부 호출
        return find(id); // ❌ 캐시 AOP 미적용
    }
}
 
  • @CacheEvict로 캐시를 지웠는데,
  • 같은 트랜잭션 안에서 다시 @Cacheable 메서드를 호출하면
    이전 캐시가 남아있거나 새 캐시가 저장되지 않는 것처럼 보이는 문제가 발생합니다.

 

이유

  1. Spring AOP 프록시의 한계
    • @Cacheable / @CacheEvict는 Spring AOP 프록시 기반으로 동작합니다.
    • 즉, 자기 자신 내부 호출일 경우 캐시 어노테이션이 동작하지 않습니다.
      (트랜잭션과 동일한 원리입니다.)
  2. 트랜잭션과 캐시의 타이밍 차이
    • @CacheEvict는 실제 캐시를 바로 지워버립니다.
    • 하지만 그 이후 @Cacheable 호출이 같은 트랜잭션 안이라면
      DB는 아직 커밋되지 않은 상태일 수도 있고,
      캐시 갱신이 의도대로 안 보일 수 있습니다.

✅ 해결 방법

 

① 서로 다른 Bean으로 분리

=> @Cacheable과 @CacheEvict을 서로 다른 빈에서 호출해야 프록시가 적용되어 캐시 동작이 정상적으로 수행됩니다.

② AopContext.currentProxy() 사용 (비추천)

((UserService) AopContext.currentProxy()).getUser(id);


자기 자신 내부에서 프록시를 강제로 다시 거치는 방법. 하지만 코드 가독성·유지보수성이 떨어지므로 추천하지 않습니다.

 

③ 트랜잭션 분리 (REQUIRES_NEW)
캐시 삭제 → 커밋 → 새 트랜잭션에서 재조회하고 싶을 때:

@CacheEvict(value = "userCache", key = "#id") 
@Transactional(propagation = Propagation.REQUIRES_NEW) 
public void evictUserCache(Long id) { }


이러면 evictUserCache는 별도 트랜잭션으로 즉시 커밋되어다음 @Cacheable 호출 시 최신 상태로 캐시가 갱신됩니다.

 

 

요약

 

상황 원인 해결책
같은 트랜잭션 내 캐시가 안 지워짐 내부 호출로 AOP 미적용 @Cacheable과 @CacheEvict을 다른 Bean으로 분리
캐시가 지워졌는데 DB 반영 전이라 데이터 이상 트랜잭션 커밋 전 캐시 접근 REQUIRES_NEW로 트랜잭션 분리
즉시 캐시 삭제 원함 기본 after-invocation beforeInvocation = true